Implement vCard 2.1 to 3.0 conversion in QRCodeParser and enhance CreateQRCodeView for improved vCard generation; update QRCodeDetailView and documentation to reflect new vCard structure and support additional fields like company, title, and address.

main
v504 2 months ago
parent 335ccd25d2
commit 4464897237

@ -1,6 +1,130 @@
import Foundation
import UIKit
// MARK: - vCard
struct VCardConverter {
/// vCard 2.13.0
static func convertVCard21To30(_ vcard21: String) -> String {
let lines = vcard21.components(separatedBy: .newlines)
var vcard30 = "BEGIN:VCARD\nVERSION:3.0\n"
for line in lines {
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
if trimmedLine.isEmpty || trimmedLine.hasPrefix("BEGIN:") || trimmedLine.hasPrefix("VERSION:") || trimmedLine.hasPrefix("END:") {
continue
}
// N ()
if trimmedLine.hasPrefix("N:") {
let nameValue = String(trimmedLine.dropFirst(2))
let nameParts = nameValue.components(separatedBy: ";")
if nameParts.count >= 2 {
let lastName = nameParts[0]
let firstName = nameParts[1]
vcard30 += "N:\(lastName);\(firstName);;;\n"
vcard30 += "FN:\(firstName) \(lastName)\n"
}
}
// TEL ()
else if trimmedLine.hasPrefix("TEL") {
let telValue = String(trimmedLine.dropFirst(3))
if telValue.hasPrefix(";TYPE=") {
let typeStart = telValue.firstIndex(of: "=")!
let typeEnd = telValue.firstIndex(of: ":") ?? telValue.endIndex
let type = String(telValue[telValue.index(after: typeStart)..<typeEnd])
let number = String(telValue[telValue.index(after: typeEnd)...])
vcard30 += "TEL;TYPE=\(type):\(number)\n"
} else if telValue.hasPrefix(":") {
let number = String(telValue.dropFirst())
vcard30 += "TEL:\(number)\n"
}
}
// EMAIL ()
else if trimmedLine.hasPrefix("EMAIL") {
let emailValue = String(trimmedLine.dropFirst(5))
if emailValue.hasPrefix(";TYPE=") {
let typeStart = emailValue.firstIndex(of: "=")!
let typeEnd = emailValue.firstIndex(of: ":") ?? emailValue.endIndex
let type = String(emailValue[emailValue.index(after: typeStart)..<typeEnd])
let email = String(emailValue[emailValue.index(after: typeEnd)...])
vcard30 += "EMAIL;TYPE=\(type):\(email)\n"
} else if emailValue.hasPrefix(":") {
let email = String(emailValue.dropFirst())
vcard30 += "EMAIL:\(email)\n"
}
}
// ADR ()
else if trimmedLine.hasPrefix("ADR") {
let adrValue = String(trimmedLine.dropFirst(3))
if adrValue.hasPrefix(";TYPE=") {
let typeStart = adrValue.firstIndex(of: "=")!
let typeEnd = adrValue.firstIndex(of: ":") ?? adrValue.endIndex
let type = String(adrValue[adrValue.index(after: typeStart)..<typeEnd])
let address = String(adrValue[adrValue.index(after: typeEnd)...])
vcard30 += "ADR;TYPE=\(type):\(address)\n"
} else if adrValue.hasPrefix(":") {
let address = String(adrValue.dropFirst())
vcard30 += "ADR:\(address)\n"
}
}
//
else if trimmedLine.contains(":") {
let colonIndex = trimmedLine.firstIndex(of: ":")!
let fieldName = String(trimmedLine[..<colonIndex])
let fieldValue = String(trimmedLine[trimmedLine.index(after: colonIndex)...])
//
if !["N", "TEL", "EMAIL", "ADR"].contains(fieldName) {
vcard30 += "\(fieldName):\(fieldValue)\n"
}
}
}
vcard30 += "END:VCARD"
return vcard30
}
/// vCard
static func detectVCardVersion(_ vcard: String) -> String {
let lines = vcard.components(separatedBy: .newlines)
for line in lines {
if line.hasPrefix("VERSION:") {
return String(line.dropFirst(8))
}
}
return "3.0" //
}
/// vCard3.0
static func normalizeVCard(_ vcard: String) -> String {
let version = detectVCardVersion(vcard)
if version == "2.1" {
return convertVCard21To30(vcard)
}
return vcard
}
/// vCard
static func testVCardConversion() {
let vcard21 = """
BEGIN:VCARD
VERSION:2.1
N:Surname;Givenname;;;
FN:Givenname Surname
TEL;TYPE=WORK:123-456-7890
EMAIL;TYPE=PREF:test@example.com
END:VCARD
"""
let converted = convertVCard21To30(vcard21)
print("Original vCard 2.1:")
print(vcard21)
print("\nConverted to vCard 3.0:")
print(converted)
}
}
// MARK: -
class QRCodeParser {
@ -177,27 +301,71 @@ class QRCodeParser {
// MARK: - vCard
private static func parseVCard(_ content: String) -> ParsedQRData {
let lines = content.components(separatedBy: .newlines)
// vCard3.0
let normalizedVCard = VCardConverter.normalizeVCard(content)
let lines = normalizedVCard.components(separatedBy: .newlines)
var name = ""
var phone = ""
var email = ""
var company = ""
var title = ""
var address = ""
var website = ""
for line in lines {
if line.hasPrefix("FN:") {
name = String(line.dropFirst(3))
} else if line.hasPrefix("TEL:") {
phone = String(line.dropFirst(4))
} else if line.hasPrefix("EMAIL:") {
email = String(line.dropFirst(6))
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
if trimmedLine.hasPrefix("FN:") {
name = String(trimmedLine.dropFirst(3))
} else if trimmedLine.hasPrefix("TEL") {
let telValue = String(trimmedLine.dropFirst(3))
if telValue.contains(":") {
let number = telValue.components(separatedBy: ":").last ?? ""
phone = number
}
} else if trimmedLine.hasPrefix("EMAIL") {
let emailValue = String(trimmedLine.dropFirst(5))
if emailValue.contains(":") {
let emailAddress = emailValue.components(separatedBy: ":").last ?? ""
email = emailAddress
}
} else if trimmedLine.hasPrefix("ORG:") {
company = String(trimmedLine.dropFirst(4))
} else if trimmedLine.hasPrefix("TITLE:") {
title = String(trimmedLine.dropFirst(6))
} else if trimmedLine.hasPrefix("ADR") {
let adrValue = String(trimmedLine.dropFirst(3))
if adrValue.contains(":") {
let addressParts = adrValue.components(separatedBy: ":")
if addressParts.count > 1 {
let addressComponents = addressParts[1].components(separatedBy: ";")
if addressComponents.count >= 3 {
address = "\(addressComponents[2]) \(addressComponents[1])"
}
}
}
} else if trimmedLine.hasPrefix("URL:") {
website = String(trimmedLine.dropFirst(4))
}
}
let title = "联系人信息"
let subtitle = "姓名: \(name)\n电话: \(phone)\n邮箱: \(email)"
var subtitle = ""
if !name.isEmpty { subtitle += "姓名: \(name)\n" }
if !phone.isEmpty { subtitle += "电话: \(phone)\n" }
if !email.isEmpty { subtitle += "邮箱: \(email)\n" }
if !company.isEmpty { subtitle += "公司: \(company)\n" }
if !title.isEmpty { subtitle += "职位: \(title)\n" }
if !address.isEmpty { subtitle += "地址: \(address)\n" }
if !website.isEmpty { subtitle += "网站: \(website)\n" }
//
if subtitle.hasSuffix("\n") {
subtitle = String(subtitle.dropLast())
}
return ParsedQRData(
type: .vcard,
title: title,
title: "联系人信息",
subtitle: subtitle,
icon: "person.crop.rectangle"
)

@ -390,27 +390,45 @@ struct CreateQRCodeView: View {
return "WIFI:T:\(wifiEncryptionType.rawValue);S:\(wifiSSID);P:\(wifiPassword);;"
case .vcard:
var vcard = "BEGIN:VCARD\nVERSION:3.0\n"
// (NFN)
if !contactFirstName.isEmpty || !contactLastName.isEmpty {
vcard += "FN:\(contactFirstName) \(contactLastName)\n"
let lastName = contactLastName.isEmpty ? "" : contactLastName
let firstName = contactFirstName.isEmpty ? "" : contactFirstName
vcard += "N:\(lastName);\(firstName);;;\n"
vcard += "FN:\(firstName) \(lastName)\n"
}
//
if !contactPhone.isEmpty {
vcard += "TEL:\(contactPhone)\n"
vcard += "TEL;TYPE=WORK,CELL:\(contactPhone)\n"
}
//
if !contactEmail.isEmpty {
vcard += "EMAIL:\(contactEmail)\n"
vcard += "EMAIL;TYPE=PREF,INTERNET:\(contactEmail)\n"
}
//
if !contactCompany.isEmpty {
vcard += "ORG:\(contactCompany)\n"
}
//
if !contactTitle.isEmpty {
vcard += "TITLE:\(contactTitle)\n"
}
//
if !contactAddress.isEmpty {
vcard += "ADR:\(contactAddress)\n"
vcard += "ADR;TYPE=WORK:;;\(contactAddress);;;;\n"
}
//
if !contactWebsite.isEmpty {
vcard += "URL:\(contactWebsite)\n"
}
vcard += "END:VCARD"
return vcard
case .mecard:

@ -404,9 +404,14 @@ private enum PreviewData {
let content = """
BEGIN:VCARD
VERSION:3.0
N:Doe;John;;;
FN:John Doe
TEL:+1234567890
EMAIL:example@example.com
TEL;TYPE=WORK,CELL:(123) 456-7890
EMAIL;TYPE=PREF,INTERNET:john.doe@example.com
ORG:Example Company
TITLE:Software Engineer
ADR;TYPE=WORK:;;123 Main St;Anytown;CA;12345;USA
URL:https://example.com
END:VCARD
""".trimmingCharacters(in: .whitespacesAndNewlines)
return makeBaseItem(in: context, content: content, qrType: .vcard)

@ -57,11 +57,18 @@ SMSTO:<电话号码>:<短信内容>
// 联系人信息 (vCard)
BEGIN:VCARD
VERSION:3.0
N:Doe;John;;;
FN:John Doe
TEL:+1234567890
EMAIL:example@example.com
TEL;TYPE=WORK,CELL:(123) 456-7890
EMAIL;TYPE=PREF,INTERNET:john.doe@example.com
ORG:Example Company
TITLE:Software Engineer
ADR;TYPE=WORK:;;123 Main St;Anytown;CA;12345;USA
URL:https://example.com
END:VCARD
// 支持vCard 2.1到3.0的自动转换
// 联系人信息 (MeCard)
MECARD:N:<姓名>;TEL:<电话>;EMAIL:<邮箱>;ADR:<地址>;;
示例: MECARD:N:John Doe;TEL:+1234567890;EMAIL:example@example.com;;

Loading…
Cancel
Save