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 10 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?
// Email
private enum EmailField: Hashable {
case address, subject, body, cc, bcc
}
@FocusState private var focusedEmailField: EmailInputView.EmailField?
// 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()
}
//
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)
InputTitleView.required("预览", icon: "eye")
.padding(.horizontal, 20)
// 使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 .text:
return "输入任意文本内容..."
case .url:
return "输入网址..."
case .mail:
return "输入邮件内容..."
case .phone:
return "输入电话号码..."
case .sms:
return "输入短信内容..."
case .wifi:
return "输入WiFi信息..."
case .vcard:
return "输入联系人信息..."
case .mecard:
return "输入联系人信息..."
case .location:
return "输入地理位置..."
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信息..."
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 generateQRCodeImage() -> UIImage? {
//
let hasContent: Bool
private func canCreateQRCode() -> Bool {
switch selectedQRCodeType {
case .mail:
hasContent = !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
return !emailAddress.isEmpty && !emailSubject.isEmpty && !emailBody.isEmpty
case .wifi:
return !wifiSSID.isEmpty
case .vcard, .mecard:
return !contactFirstName.isEmpty || !contactLastName.isEmpty
case .location:
return !locationLatitude.isEmpty && !locationLongitude.isEmpty
case .calendar:
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:
hasContent = !content.isEmpty
return !content.isEmpty
}
}
guard hasContent else { return nil }
private func getContentHint() -> String {
InputComponentFactory.getPlaceholderText(for: selectedQRCodeType)
}
//
let formattedContent = formatContentForQRCodeType()
private func generateQRCodeImage() -> UIImage? {
guard canCreateQRCode() 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