Refactor CreateQRCodeView to support multiple QR code types with dynamic input handling; introduced new fields for WiFi, contact, location, calendar, social media, and phone inputs. Enhanced initial focus setup and input validation for improved user experience and modularity.

main
v504 2 months ago
parent 4e57ba1abd
commit 972774adb1

@ -102,7 +102,8 @@ struct InputComponentFactory {
eventDescription: eventDescription,
startDate: startDate,
endDate: endDate,
location: location
location: location,
focusedField: focusedCalendarField
)
)
@ -150,7 +151,7 @@ struct InputComponentFactory {
}
//
private static func getPlaceholderText(for qrCodeType: QRCodeType) -> String {
static func getPlaceholderText(for qrCodeType: QRCodeType) -> String {
switch qrCodeType {
case .text:
return "输入任意文本内容..."

@ -10,11 +10,8 @@ struct CreateQRCodeView: View {
//
let selectedQRCodeType: QRCodeType
//
@State private var content = ""
@State private var showingAlert = false
@State private var alertMessage = ""
//
@FocusState private var isContentFieldFocused: Bool
// Email
@ -23,12 +20,56 @@ struct CreateQRCodeView: View {
@State private var emailBody = ""
@State private var emailCc = ""
@State private var emailBcc = ""
@FocusState private var focusedEmailField: EmailField?
@FocusState private var focusedEmailField: EmailInputView.EmailField?
// Email
private enum EmailField: Hashable {
case address, subject, body, cc, bcc
}
// WiFi
@State private var wifiSSID = ""
@State private var wifiPassword = ""
@State private var wifiEncryptionType: WiFiInputView.WiFiEncryptionType = .wpa2
@FocusState private var focusedWiFiField: WiFiInputView.WiFiField?
//
@State private var contactFirstName = ""
@State private var contactLastName = ""
@State private var contactPhone = ""
@State private var contactEmail = ""
@State private var contactCompany = ""
@State private var contactTitle = ""
@State private var contactAddress = ""
@State private var contactWebsite = ""
@FocusState private var focusedContactField: ContactInputView.ContactField?
//
@State private var locationLatitude = ""
@State private var locationLongitude = ""
@State private var locationName = ""
@FocusState private var focusedLocationField: LocationInputView.LocationField?
//
@State private var eventTitle = ""
@State private var eventDescription = ""
@State private var eventLocation = ""
@State private var startDate = Date()
@State private var endDate = Date().addingTimeInterval(3600)
@FocusState private var focusedCalendarField: CalendarInputView.CalendarField?
//
@State private var socialUsername = ""
@State private var socialMessage = ""
@FocusState private var focusedSocialField: SocialInputView.SocialField?
//
@State private var phoneNumber = ""
@State private var phoneMessage = ""
@FocusState private var focusedPhoneField: PhoneInputView.PhoneField?
// URL
@State private var urlString = ""
@FocusState private var isURLFieldFocused: Bool
//
@State private var showingAlert = false
@State private var alertMessage = ""
var body: some View {
VStack(spacing: 0) {
@ -47,345 +88,92 @@ struct CreateQRCodeView: View {
Button("确定") { }
} message: { Text(alertMessage) }
.onAppear {
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if selectedQRCodeType == .mail {
focusedEmailField = .address
} else {
isContentFieldFocused = true
}
}
setupInitialFocus()
}
.onTapGesture {
//
if selectedQRCodeType == .mail {
focusedEmailField = nil
} else {
isContentFieldFocused = false
}
hideKeyboard()
}
}
// MARK: - UI Components
private var emailInputSection: some View {
VStack(spacing: 16) {
// Email ()
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("邮箱地址")
.font(.subheadline)
.foregroundColor(.primary)
Text("*")
.foregroundColor(.red)
Spacer()
}
TextField("user@example.com", text: $emailAddress)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
.autocapitalization(.none)
.focused($focusedEmailField, equals: .address)
}
// ()
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("主题")
.font(.subheadline)
.foregroundColor(.primary)
Text("*")
.foregroundColor(.red)
Spacer()
}
TextField("邮件主题", text: $emailSubject)
.textFieldStyle(RoundedBorderTextFieldStyle())
.focused($focusedEmailField, equals: .subject)
}
// ()
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("正文")
.font(.subheadline)
.foregroundColor(.primary)
Text("*")
.foregroundColor(.red)
Spacer()
}
ZStack {
TextEditor(text: $emailBody)
.frame(minHeight: 120)
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(focusedEmailField == .body ? Color.blue : Color(.systemGray4), lineWidth: 1)
)
.focused($focusedEmailField, equals: .body)
.onChange(of: emailBody) { newValue in
// 1200
if newValue.count > 1200 {
emailBody = String(newValue.prefix(1200))
}
}
//
if emailBody.isEmpty && focusedEmailField != .body {
VStack {
HStack {
Text("输入邮件正文内容...")
.foregroundColor(.secondary)
.font(.body)
Spacer()
}
Spacer()
}
.padding(16)
.allowsHitTesting(false)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
//
HStack {
Spacer()
Text("\(emailBody.count)/1200")
.font(.caption)
.foregroundColor(emailBody.count >= 1200 ? .orange : .secondary)
}
}
// CC ()
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("抄送地址")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
}
TextField("cc@example.com", text: $emailCc)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
.autocapitalization(.none)
.focused($focusedEmailField, equals: .cc)
}
// BCC ()
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("密送地址")
.font(.subheadline)
.foregroundColor(.primary)
Spacer()
}
TextField("bcc@example.com", text: $emailBcc)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.emailAddress)
.autocapitalization(.none)
.focused($focusedEmailField, equals: .bcc)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("完成") {
focusedEmailField = nil
}
.foregroundColor(.blue)
.font(.system(size: 16, weight: .medium))
}
}
}
private var inputAndPreviewSection: some View {
ScrollView {
VStack(spacing: 24) {
//
VStack(spacing: 12) {
HStack {
Image(systemName: "info.circle")
.font(.caption)
.foregroundColor(.blue)
Text(getContentHint())
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(nil)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.1))
)
}
InputHintView.info(
hint: getContentHint()
)
.padding(.horizontal, 20)
//
VStack(spacing: 16) {
HStack {
Text(selectedQRCodeType == .mail ? "邮件信息" : "输入内容")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
InputTitleView.required(
selectedQRCodeType == .mail ? "邮件信息" : "输入内容",
icon: getInputIcon()
)
.padding(.horizontal, 20)
if selectedQRCodeType == .mail {
// Email
emailInputSection
} else {
//
VStack(spacing: 8) {
ZStack {
//
TextEditor(text: $content)
.frame(minHeight: 120)
.padding(8)
.background(Color(.systemBackground))
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(isContentFieldFocused ? Color.blue : Color(.systemGray4), lineWidth: 1)
)
.focused($isContentFieldFocused)
.onChange(of: content) { newValue in
// 150
if newValue.count > 150 {
content = String(newValue.prefix(150))
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button("完成") {
isContentFieldFocused = false
}
.foregroundColor(.blue)
.font(.system(size: 16, weight: .medium))
}
}
// -
if content.isEmpty && !isContentFieldFocused {
VStack {
HStack {
Text(getPlaceholderText())
.foregroundColor(.secondary)
.font(.body)
Spacer()
}
Spacer()
}
.padding(16)
.allowsHitTesting(false)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
// -
HStack {
Spacer() // Pushes content to the right
VStack(alignment: .trailing, spacing: 4) {
//
if content.count >= 150 {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.triangle")
.font(.caption)
.foregroundColor(.orange)
Text("已达到最大字符数")
.font(.caption)
.foregroundColor(.orange)
}
} else if content.count >= 140 {
HStack(spacing: 4) {
Image(systemName: "info.circle")
.font(.caption)
.foregroundColor(.blue)
Text("接近字符限制")
.font(.caption)
.foregroundColor(.blue)
}
}
//
Text("\(content.count)/150")
.font(.caption)
.foregroundColor(getCharacterCountColor())
}
}
}
}
// 使InputComponentFactory
InputComponentFactory.createInputComponent(
for: selectedQRCodeType,
content: $content,
emailAddress: $emailAddress,
emailSubject: $emailSubject,
emailBody: $emailBody,
emailCc: $emailCc,
emailBcc: $emailBcc,
focusedEmailField: _focusedEmailField,
isContentFieldFocused: _isContentFieldFocused,
ssid: $wifiSSID,
password: $wifiPassword,
encryptionType: $wifiEncryptionType,
focusedWiFiField: _focusedWiFiField,
firstName: $contactFirstName,
lastName: $contactLastName,
phone: $contactPhone,
email: $contactEmail,
company: $contactCompany,
title: $contactTitle,
address: $contactAddress,
website: $contactWebsite,
focusedContactField: _focusedContactField,
latitude: $locationLatitude,
longitude: $locationLongitude,
locationName: $locationName,
focusedLocationField: _focusedLocationField,
eventTitle: $eventTitle,
eventDescription: $eventDescription,
startDate: $startDate,
endDate: $endDate,
location: $eventLocation,
focusedCalendarField: _focusedCalendarField,
username: $socialUsername,
message: $socialMessage,
focusedSocialField: _focusedSocialField,
phoneNumber: $phoneNumber,
phoneMessage: $phoneMessage,
focusedPhoneField: _focusedPhoneField,
url: $urlString,
isUrlFieldFocused: _isURLFieldFocused
)
.padding(.horizontal, 20)
}
.padding(.horizontal, 20)
//
if selectedQRCodeType == .mail ? (!emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty) : !content.isEmpty {
if canCreateQRCode() {
VStack(spacing: 16) {
HStack {
Text("预览")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
InputTitleView.required("预览", icon: "eye")
.padding(.horizontal, 20)
//
VStack(spacing: 16) {
//
if let qrImage = generateQRCodeImage() {
Image(uiImage: qrImage)
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.background(Color.white)
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
}
//
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("内容")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(selectedQRCodeType.displayName)
.font(.caption)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.orange.opacity(0.1))
.foregroundColor(.orange)
.cornerRadius(4)
}
Text(formatContentForQRCodeType())
.font(.body)
.foregroundColor(.primary)
.textSelection(.enabled)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2)
// 使QRCodePreviewView
QRCodePreviewView(
qrCodeImage: generateQRCodeImage(),
formattedContent: formatContentForQRCodeType(),
qrCodeType: selectedQRCodeType
)
.padding(.horizontal, 20)
}
.padding(.horizontal, 20)
}
Spacer(minLength: 100)
@ -397,133 +185,106 @@ struct CreateQRCodeView: View {
// MARK: - Helper Methods
private func canCreateQRCode() -> Bool {
switch selectedQRCodeType {
case .mail:
// Email
return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
default:
//
return !content.isEmpty
}
}
private func getCharacterCountColor() -> Color {
if content.count >= 150 {
return .orange
} else if content.count >= 140 {
return .blue
} else {
return .secondary
private func setupInitialFocus() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
switch selectedQRCodeType {
case .mail:
focusedEmailField = .address
case .wifi:
focusedWiFiField = .ssid
case .vcard, .mecard:
focusedContactField = .firstName
case .location:
focusedLocationField = .latitude
case .calendar:
focusedCalendarField = .title
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
focusedSocialField = .username
case .phone, .sms:
focusedPhoneField = .phoneNumber
case .url:
isURLFieldFocused = true
default:
isContentFieldFocused = true
}
}
}
private func getContentHint() -> String {
private func hideKeyboard() {
switch selectedQRCodeType {
case .text:
return "输入任意文本内容"
case .url:
return "输入网址https://www.example.com"
case .mail:
return "填写邮件信息,邮箱地址、主题、正文为必填项"
case .phone:
return "输入电话号码,如:+86 138 0013 8000"
case .sms:
return "输入短信内容Hello World"
focusedEmailField = nil
case .wifi:
return "输入WiFi信息SSID:MyWiFi,Password:12345678"
case .vcard:
return "输入联系人信息"
case .mecard:
return "输入联系人信息(简化版)"
focusedWiFiField = nil
case .vcard, .mecard:
focusedContactField = nil
case .location:
return "输入地理位置40.7128,-74.0060"
focusedLocationField = nil
case .calendar:
return "输入日历事件信息"
case .instagram:
return "输入Instagram用户名或链接"
case .facebook:
return "输入Facebook用户名或链接"
case .spotify:
return "输入Spotify歌曲或播放列表链接"
case .twitter:
return "输入Twitter用户名或链接"
case .whatsapp:
return "输入WhatsApp消息内容"
case .viber:
return "输入Viber消息内容"
case .snapchat:
return "输入Snapchat用户名"
case .tiktok:
return "输入TikTok用户名或链接"
focusedCalendarField = nil
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
focusedSocialField = nil
case .phone, .sms:
focusedPhoneField = nil
case .url:
isURLFieldFocused = false
default:
isContentFieldFocused = false
}
}
private func getPlaceholderText() -> String {
private func getInputIcon() -> String {
switch selectedQRCodeType {
case .mail: return "envelope"
case .wifi: return "wifi"
case .vcard, .mecard: return "person"
case .location: return "location"
case .calendar: return "calendar"
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok: return "globe"
case .phone, .sms: return "phone"
case .url: return "link"
default: return "textformat"
}
}
private func canCreateQRCode() -> Bool {
switch selectedQRCodeType {
case .text:
return "输入任意文本内容..."
case .url:
return "输入网址..."
case .mail:
return "输入邮件内容..."
case .phone:
return "输入电话号码..."
case .sms:
return "输入短信内容..."
return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
case .wifi:
return "输入WiFi信息..."
case .vcard:
return "输入联系人信息..."
case .mecard:
return "输入联系人信息..."
return !wifiSSID.isEmpty
case .vcard, .mecard:
return !contactFirstName.isEmpty || !contactLastName.isEmpty
case .location:
return "输入地理位置..."
return !locationLatitude.isEmpty && !locationLongitude.isEmpty
case .calendar:
return "输入日历事件信息..."
case .instagram:
return "输入Instagram信息..."
case .facebook:
return "输入Facebook信息..."
case .spotify:
return "输入Spotify信息..."
case .twitter:
return "输入Twitter信息..."
case .whatsapp:
return "输入WhatsApp信息..."
case .viber:
return "输入Viber信息..."
case .snapchat:
return "输入Snapchat信息..."
case .tiktok:
return "输入TikTok信息..."
return !eventTitle.isEmpty
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
return !socialUsername.isEmpty
case .phone, .sms:
return !phoneNumber.isEmpty
case .url:
return !urlString.isEmpty
default:
return !content.isEmpty
}
}
private func getContentHint() -> String {
InputComponentFactory.getPlaceholderText(for: selectedQRCodeType)
}
private func generateQRCodeImage() -> UIImage? {
//
let hasContent: Bool
switch selectedQRCodeType {
case .mail:
hasContent = !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
default:
hasContent = !content.isEmpty
}
guard canCreateQRCode() else { return nil }
guard hasContent else { return nil }
//
let formattedContent = formatContentForQRCodeType()
//
let data = formattedContent.data(using: .utf8)
let qrFilter = CIFilter.qrCodeGenerator()
qrFilter.setValue(data, forKey: "inputMessage")
qrFilter.setValue("H", forKey: "inputCorrectionLevel") //
qrFilter.setValue("H", forKey: "inputCorrectionLevel")
guard let outputImage = qrFilter.outputImage else { return nil }
// UIImage
let context = CIContext()
guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { return nil }
@ -535,7 +296,7 @@ struct CreateQRCodeView: View {
case .text:
return content
case .url:
return content.hasPrefix("http") ? content : "https://\(content)"
return urlString.hasPrefix("http") ? urlString : "https://\(urlString)"
case .mail:
var mailtoURL = "mailto:\(emailAddress)"
var queryParams: [String] = []
@ -562,35 +323,97 @@ struct CreateQRCodeView: View {
return mailtoURL
case .phone:
return "tel:\(content)"
return "tel:\(phoneNumber)"
case .sms:
return "sms:\(content)"
let smsContent = phoneMessage.isEmpty ? "Hello" : phoneMessage
return "sms:\(phoneNumber):\(smsContent)"
case .wifi:
return "WIFI:T:WPA;S:\(content);P:password;;"
return "WIFI:T:\(wifiEncryptionType.rawValue);S:\(wifiSSID);P:\(wifiPassword);;"
case .vcard:
return "BEGIN:VCARD\nVERSION:3.0\nFN:\(content)\nEND:VCARD"
var vcard = "BEGIN:VCARD\nVERSION:3.0\n"
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
vcard += "FN:\(contactFirstName) \(contactLastName)\n"
}
if !contactPhone.isEmpty {
vcard += "TEL:\(contactPhone)\n"
}
if !contactEmail.isEmpty {
vcard += "EMAIL:\(contactEmail)\n"
}
if !contactCompany.isEmpty {
vcard += "ORG:\(contactCompany)\n"
}
if !contactTitle.isEmpty {
vcard += "TITLE:\(contactTitle)\n"
}
if !contactAddress.isEmpty {
vcard += "ADR:\(contactAddress)\n"
}
if !contactWebsite.isEmpty {
vcard += "URL:\(contactWebsite)\n"
}
vcard += "END:VCARD"
return vcard
case .mecard:
return "MECARD:N:\(content);;"
var mecard = "MECARD:"
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
mecard += "N:\(contactLastName),\(contactFirstName);"
}
if !contactPhone.isEmpty {
mecard += "TEL:\(contactPhone);"
}
if !contactEmail.isEmpty {
mecard += "EMAIL:\(contactEmail);"
}
if !contactCompany.isEmpty {
mecard += "ORG:\(contactCompany);"
}
if !contactAddress.isEmpty {
mecard += "ADR:\(contactAddress);"
}
if !contactWebsite.isEmpty {
mecard += "URL:\(contactWebsite);"
}
mecard += ";"
return mecard
case .location:
return "geo:\(content)"
let coords = "\(locationLatitude),\(locationLongitude)"
return locationName.isEmpty ? "geo:\(coords)" : "geo:\(coords)?q=\(locationName)"
case .calendar:
return "BEGIN:VEVENT\nSUMMARY:\(content)\nEND:VEVENT"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
var ical = "BEGIN:VEVENT\n"
ical += "SUMMARY:\(eventTitle)\n"
if !eventDescription.isEmpty {
ical += "DESCRIPTION:\(eventDescription)\n"
}
if !eventLocation.isEmpty {
ical += "LOCATION:\(eventLocation)\n"
}
ical += "DTSTART:\(dateFormatter.string(from: startDate))\n"
ical += "DTEND:\(dateFormatter.string(from: endDate))\n"
ical += "END:VEVENT"
return ical
case .instagram:
return "https://instagram.com/\(content)"
return "https://instagram.com/\(socialUsername)"
case .facebook:
return "https://facebook.com/\(content)"
return "https://facebook.com/\(socialUsername)"
case .spotify:
return content.hasPrefix("http") ? content : "https://open.spotify.com/track/\(content)"
return socialUsername.hasPrefix("http") ? socialUsername : "https://open.spotify.com/track/\(socialUsername)"
case .twitter:
return "https://twitter.com/\(content)"
return "https://twitter.com/\(socialUsername)"
case .whatsapp:
return "https://wa.me/\(content)"
let message = socialMessage.isEmpty ? "Hello" : socialMessage
return "https://wa.me/\(socialUsername)?text=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? message)"
case .viber:
return "viber://chat?number=\(content)"
let message = socialMessage.isEmpty ? "Hello" : socialMessage
return "viber://chat?number=\(socialUsername)&text=\(message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? message)"
case .snapchat:
return "https://snapchat.com/add/\(content)"
return "https://snapchat.com/add/\(socialUsername)"
case .tiktok:
return "https://tiktok.com/@\(content)"
return "https://tiktok.com/@\(socialUsername)"
}
}
@ -615,6 +438,20 @@ struct CreateQRCodeView: View {
mailContent += "\n密送: \(emailBcc)"
}
historyItem.content = mailContent
case .wifi:
historyItem.content = "WiFi: \(wifiSSID) (\(wifiEncryptionType.displayName))"
case .vcard, .mecard:
historyItem.content = "联系人: \(contactFirstName) \(contactLastName)"
case .location:
historyItem.content = "位置: \(locationLatitude), \(locationLongitude)"
case .calendar:
historyItem.content = "事件: \(eventTitle)"
case .instagram, .facebook, .spotify, .twitter, .snapchat, .tiktok:
historyItem.content = "\(selectedQRCodeType.displayName): \(socialUsername)"
case .phone, .sms:
historyItem.content = "电话: \(phoneNumber)"
case .url:
historyItem.content = "URL: \(urlString)"
default:
historyItem.content = content
}

@ -169,7 +169,6 @@ struct QRCodeDetailView: View {
// MARK: -
private var originalContentSection: some View {
#if DEBUG
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "doc.text")
@ -200,9 +199,6 @@ struct QRCodeDetailView: View {
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
#else
EmptyView()
#endif
}
// MARK: -

Loading…
Cancel
Save