You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

816 lines
33 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 {
// MARK: -
static func parseQRCode(_ content: String) -> ParsedQRData {
let trimmedContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
// Wi-Fi
if trimmedContent.uppercased().hasPrefix("WIFI:") {
return parseWiFi(trimmedContent)
}
// Email
if trimmedContent.lowercased().hasPrefix("mailto:") {
return parseEmail(trimmedContent)
}
// Phone
if trimmedContent.lowercased().hasPrefix("tel:") {
return parsePhone(trimmedContent)
}
// SMS
if trimmedContent.uppercased().hasPrefix("SMSTO:") {
return parseSMS(trimmedContent)
}
// vCard
if trimmedContent.uppercased().hasPrefix("BEGIN:VCARD") {
return parseVCard(trimmedContent)
}
// MeCard
if trimmedContent.uppercased().hasPrefix("MECARD:") {
return parseMeCard(trimmedContent)
}
// Calendar
if trimmedContent.uppercased().hasPrefix("BEGIN:VEVENT") {
return parseCalendar(trimmedContent)
}
// Instagram
if trimmedContent.lowercased().hasPrefix("instagram://user?username=") {
return parseInstagram(trimmedContent)
}
// Facebook
if trimmedContent.lowercased().hasPrefix("fb://profile/") {
return parseFacebook(trimmedContent)
}
// Spotify
if trimmedContent.lowercased().hasPrefix("spotify:search:") {
return parseSpotify(trimmedContent)
}
// X (Twitter)
if trimmedContent.lowercased().hasPrefix("twitter://user?screen_name=") || trimmedContent.lowercased().contains("x.com") || trimmedContent.lowercased().contains("twitter.com") {
return parseTwitter(trimmedContent)
}
// WhatsApp
if trimmedContent.lowercased().hasPrefix("whatsapp://send?phone=") {
return parseWhatsApp(trimmedContent)
}
// Viber
if trimmedContent.lowercased().hasPrefix("viber://add?number=") {
return parseViber(trimmedContent)
}
// Snapchat
if trimmedContent.lowercased().hasPrefix("snapchat://") {
return parseSnapchat(trimmedContent)
}
// TikTok
if trimmedContent.lowercased().contains("tiktok.com") || trimmedContent.lowercased().contains("www.tiktok.com") {
return parseTikTok(trimmedContent)
}
// URL (URL)
if isValidURL(trimmedContent) {
return parseURL(trimmedContent)
}
// Location
if trimmedContent.lowercased().hasPrefix("geo:") {
return parseLocation(trimmedContent)
}
//
return ParsedQRData(
type: .text,
title: "text_information".localized,
subtitle: trimmedContent.count > 50 ? String(trimmedContent.prefix(50)) + "..." : trimmedContent,
icon: "text.quote"
)
}
// MARK: - Wi-Fi
private static func parseWiFi(_ content: String) -> ParsedQRData {
let wifiInfo = content.replacingOccurrences(of: "WIFI:", with: "", options: .caseInsensitive)
let components = wifiInfo.components(separatedBy: ";")
var ssid = ""
var password = ""
var encryption = "WPA"
for component in components {
if component.uppercased().hasPrefix("S:") {
ssid = String(component.dropFirst(2))
} else if component.uppercased().hasPrefix("P:") {
password = String(component.dropFirst(2))
} else if component.uppercased().hasPrefix("T:") {
encryption = String(component.dropFirst(2))
}
}
let title = "wifi_network".localized
let subtitle = String(format: "wifi_network_info".localized, ssid, encryption, password)
// WiFi
let wifiDetails = WiFiDetails(ssid: ssid, password: password, encryption: encryption)
let extraData = try? JSONEncoder().encode(wifiDetails)
return ParsedQRData(
type: .wifi,
title: title,
subtitle: subtitle,
icon: "wifi",
extraData: extraData
)
}
// MARK: - Email
private static func parseEmail(_ content: String) -> ParsedQRData {
let emailInfo = content.replacingOccurrences(of: "mailto:", with: "", options: .caseInsensitive)
let components = emailInfo.components(separatedBy: "?")
let emailAddress = components.first ?? ""
var subject = ""
var body = ""
//
if components.count > 1 {
let queryString = components[1]
let queryParams = queryString.components(separatedBy: "&")
for param in queryParams {
let keyValue = param.components(separatedBy: "=")
if keyValue.count == 2 {
let key = keyValue[0].lowercased()
let value = keyValue[1].removingPercentEncoding ?? keyValue[1]
if key == "subject" {
subject = value
} else if key == "body" {
body = value
}
}
}
}
//
var subtitle = emailAddress
if !subject.isEmpty {
subtitle += String(format: "\n主题: %@", subject)
}
if !body.isEmpty {
let truncatedBody = body.count > 100 ? String(body.prefix(100)) + "..." : body
subtitle += String(format: "\n内容: %@", truncatedBody)
}
// Email
let emailDetails = EmailDetails(emailAddress: emailAddress, subject: subject, body: body)
let extraData = try? JSONEncoder().encode(emailDetails)
return ParsedQRData(
type: .mail,
title: "email_address".localized,
subtitle: subtitle,
icon: "envelope",
extraData: extraData
)
}
// MARK: - Phone
private static func parsePhone(_ content: String) -> ParsedQRData {
let phone = content.replacingOccurrences(of: "tel:", with: "", options: .caseInsensitive)
return ParsedQRData(
type: .phone,
title: "phone_number".localized,
subtitle: phone,
icon: "phone"
)
}
// MARK: - SMS
private static func parseSMS(_ content: String) -> ParsedQRData {
let smsInfo = content.replacingOccurrences(of: "SMSTO:", with: "", options: .caseInsensitive)
let components = smsInfo.components(separatedBy: ":")
let phone = components.first?.trimmingCharacters(in: .whitespaces) ?? ""
let message = components.count > 1 ? components[1].trimmingCharacters(in: .whitespaces) : ""
let title = "sms".localized
var subtitle = ""
//
if !phone.isEmpty {
subtitle += String(format: "📱 %@", phone)
}
if !message.isEmpty {
if !subtitle.isEmpty { subtitle += "\n" }
let truncatedMessage = message.count > 100 ? String(message.prefix(100)) + "..." : message
subtitle += String(format: "💬 %@", truncatedMessage)
}
// SMS
let smsDetails = SMSDetails(phoneNumber: phone, message: message)
let extraData = try? JSONEncoder().encode(smsDetails)
return ParsedQRData(
type: .sms,
title: title,
subtitle: subtitle,
icon: "message",
extraData: extraData
)
}
// MARK: - vCard
private static func parseVCard(_ content: String) -> ParsedQRData {
// vCard3.0
let normalizedVCard = VCardConverter.normalizeVCard(content)
let contactInfo = parseContactInfoFromVCard(normalizedVCard)
var subtitle = ""
if !contactInfo.name.isEmpty { subtitle += String(format: "contact_name".localized, contactInfo.name) + "\n" }
if !contactInfo.phoneNumber.isEmpty { subtitle += String(format: "contact_phone".localized, contactInfo.phoneNumber) + "\n" }
if !contactInfo.email.isEmpty { subtitle += String(format: "contact_email".localized, contactInfo.email) + "\n" }
if !contactInfo.organization.isEmpty { subtitle += String(format: "contact_company".localized, contactInfo.organization) + "\n" }
if !contactInfo.title.isEmpty { subtitle += String(format: "contact_title".localized, contactInfo.title) + "\n" }
if !contactInfo.address.isEmpty { subtitle += String(format: "contact_address".localized, contactInfo.address) + "\n" }
//
if subtitle.hasSuffix("\n") {
subtitle = String(subtitle.dropLast())
}
//
let extraData = try? JSONEncoder().encode(contactInfo)
return ParsedQRData(
type: .vcard,
title: "contact_information".localized,
subtitle: subtitle,
icon: "person.crop.rectangle",
extraData: extraData
)
}
// MARK: - MeCard
private static func parseMeCard(_ content: String) -> ParsedQRData {
let contactInfo = parseContactInfoFromMeCard(content)
var subtitle = ""
if !contactInfo.name.isEmpty { subtitle += String(format: "contact_name".localized, contactInfo.name) + "\n" }
if !contactInfo.phoneNumber.isEmpty { subtitle += String(format: "contact_phone".localized, contactInfo.phoneNumber) + "\n" }
if !contactInfo.email.isEmpty { subtitle += String(format: "contact_email".localized, contactInfo.email) + "\n" }
if !contactInfo.organization.isEmpty { subtitle += String(format: "contact_company".localized, contactInfo.organization) + "\n" }
if !contactInfo.title.isEmpty { subtitle += String(format: "contact_title".localized, contactInfo.title) + "\n" }
if !contactInfo.address.isEmpty { subtitle += String(format: "contact_address".localized, contactInfo.address) + "\n" }
//
if subtitle.hasSuffix("\n") {
subtitle = String(subtitle.dropLast())
}
//
let extraData = try? JSONEncoder().encode(contactInfo)
return ParsedQRData(
type: .mecard,
title: "contact_information".localized,
subtitle: subtitle,
icon: "person.crop.rectangle",
extraData: extraData
)
}
// MARK: - Calendar
private static func parseCalendar(_ content: String) -> ParsedQRData {
let lines = content.components(separatedBy: .newlines)
var summary = ""
var startTime = ""
var endTime = ""
var location = ""
var description = ""
for line in lines {
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
if trimmedLine.uppercased().hasPrefix("SUMMARY:") {
summary = String(trimmedLine.dropFirst(8)).trimmingCharacters(in: .whitespaces)
} else if trimmedLine.uppercased().hasPrefix("DTSTART:") {
startTime = String(trimmedLine.dropFirst(8)).trimmingCharacters(in: .whitespaces)
} else if trimmedLine.uppercased().hasPrefix("DTEND:") {
endTime = String(trimmedLine.dropFirst(6)).trimmingCharacters(in: .whitespaces)
} else if trimmedLine.uppercased().hasPrefix("LOCATION:") {
location = String(trimmedLine.dropFirst(9)).trimmingCharacters(in: .whitespaces)
} else if trimmedLine.uppercased().hasPrefix("DESCRIPTION:") {
description = String(trimmedLine.dropFirst(12)).trimmingCharacters(in: .whitespaces)
}
}
//
let formattedStartTime = formatCalendarTime(startTime)
let formattedEndTime = formatCalendarTime(endTime)
let title = "calendar_event".localized
var subtitle = ""
//
if !summary.isEmpty {
subtitle += summary
}
if !formattedStartTime.isEmpty {
if !subtitle.isEmpty { subtitle += "\n" }
subtitle += String(format: "📅 %@", formattedStartTime)
}
if !formattedEndTime.isEmpty && formattedEndTime != formattedStartTime {
subtitle += String(format: " - %@", formattedEndTime)
}
if !location.isEmpty {
if !subtitle.isEmpty { subtitle += "\n" }
subtitle += String(format: "📍 %@", location)
}
if !description.isEmpty {
if !subtitle.isEmpty { subtitle += "\n" }
let truncatedDescription = description.count > 100 ? String(description.prefix(100)) + "..." : description
subtitle += String(format: "📝 %@", truncatedDescription)
}
//
let calendarDetails = CalendarDetails(summary: summary, startTime: startTime, endTime: endTime, location: location, description: description)
let extraData = try? JSONEncoder().encode(calendarDetails)
return ParsedQRData(
type: .calendar,
title: title,
subtitle: subtitle,
icon: "calendar",
extraData: extraData
)
}
// MARK: -
private static func formatCalendarTime(_ timeString: String) -> String {
guard !timeString.isEmpty else { return "" }
let dateFormatter = DateFormatter()
//
let formats = [
"yyyyMMdd'T'HHmmss", // 20241201T140000
"yyyyMMdd'T'HHmmss'Z'", // Z20241201T140000Z
"yyyyMMdd", // 20241201
"yyyyMMdd'T'HHmm", // 20241201T1400
"yyyy-MM-dd'T'HH:mm:ss", // ISO2024-12-01T14:00:00
"yyyy-MM-dd'T'HH:mm:ss'Z'" // ISOZ2024-12-01T14:00:00Z
]
for format in formats {
dateFormatter.dateFormat = format
if let date = dateFormatter.date(from: timeString) {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
displayFormatter.timeStyle = .short
return displayFormatter.string(from: date)
}
}
//
return timeString
}
// MARK: - Instagram
private static func parseInstagram(_ content: String) -> ParsedQRData {
var username = ""
if content.lowercased().hasPrefix("instagram://user?username=") {
username = content.replacingOccurrences(of: "instagram://user?username=", with: "", options: .caseInsensitive)
} else if content.lowercased().contains("instagram.com") {
// URL
let components = content.components(separatedBy: "/")
if let lastComponent = components.last, !lastComponent.isEmpty && !lastComponent.contains("?") {
username = lastComponent
}
}
return ParsedQRData(
type: .instagram,
title: "instagram".localized,
subtitle: String(format: "instagram_username".localized, username),
icon: "camera"
)
}
// MARK: - Facebook
private static func parseFacebook(_ content: String) -> ParsedQRData {
var profileId = ""
if content.lowercased().hasPrefix("fb://profile/") {
profileId = content.replacingOccurrences(of: "fb://profile/", with: "", options: .caseInsensitive)
} else if content.lowercased().contains("facebook.com") {
// URLID
let components = content.components(separatedBy: "/")
if let lastComponent = components.last, !lastComponent.isEmpty && !lastComponent.contains("?") {
profileId = lastComponent
}
}
return ParsedQRData(
type: .facebook,
title: "facebook".localized,
subtitle: String(format: "facebook_profile_id".localized, profileId),
icon: "person.2"
)
}
// MARK: - Spotify
private static func parseSpotify(_ content: String) -> ParsedQRData {
var subtitle = ""
if content.lowercased().hasPrefix("spotify:search:") {
let searchQuery = content.replacingOccurrences(of: "spotify:search:", with: "", options: .caseInsensitive)
subtitle = String(format: "spotify_search_query".localized, searchQuery)
} else if content.lowercased().hasPrefix("spotify:track:") {
let trackId = content.replacingOccurrences(of: "spotify:track:", with: "", options: .caseInsensitive)
subtitle = String(format: "spotify_track".localized, trackId)
} else if content.lowercased().hasPrefix("spotify:album:") {
let albumId = content.replacingOccurrences(of: "spotify:album:", with: "", options: .caseInsensitive)
subtitle = String(format: "spotify_album".localized, albumId)
} else if content.lowercased().hasPrefix("spotify:artist:") {
let artistId = content.replacingOccurrences(of: "spotify:artist:", with: "", options: .caseInsensitive)
subtitle = String(format: "spotify_artist".localized, artistId)
} else if content.lowercased().hasPrefix("spotify:playlist:") {
let playlistId = content.replacingOccurrences(of: "spotify:playlist:", with: "", options: .caseInsensitive)
subtitle = String(format: "spotify_playlist".localized, playlistId)
} else if content.lowercased().contains("open.spotify.com") {
// URL
if content.lowercased().contains("/track/") {
let trackId = content.components(separatedBy: "/track/").last?.components(separatedBy: "?").first ?? ""
subtitle = String(format: "spotify_track".localized, trackId)
} else if content.lowercased().contains("/album/") {
let albumId = content.components(separatedBy: "/album/").last?.components(separatedBy: "?").first ?? ""
subtitle = String(format: "spotify_album".localized, albumId)
} else if content.lowercased().contains("/artist/") {
let artistId = content.components(separatedBy: "/artist/").last?.components(separatedBy: "?").first ?? ""
subtitle = String(format: "spotify_artist".localized, artistId)
} else if content.lowercased().contains("/playlist/") {
let playlistId = content.components(separatedBy: "/playlist/").last?.components(separatedBy: "?").first ?? ""
subtitle = String(format: "spotify_playlist".localized, playlistId)
}
}
return ParsedQRData(
type: .spotify,
title: "spotify".localized,
subtitle: subtitle,
icon: "music.note"
)
}
// MARK: - X (Twitter)
private static func parseTwitter(_ content: String) -> ParsedQRData {
var username = ""
if content.lowercased().hasPrefix("twitter://user?screen_name=") {
username = content.replacingOccurrences(of: "twitter://user?screen_name=", with: "", options: .caseInsensitive)
} else if content.lowercased().contains("x.com") || content.lowercased().contains("twitter.com") {
username = content.components(separatedBy: "/").dropLast().last ?? ""
}
return ParsedQRData(
type: .twitter,
title: "x".localized,
subtitle: String(format: "twitter_username".localized, username),
icon: "bird"
)
}
// MARK: - WhatsApp
private static func parseWhatsApp(_ content: String) -> ParsedQRData {
let phone = content.replacingOccurrences(of: "whatsapp://send?phone=", with: "", options: .caseInsensitive)
return ParsedQRData(
type: .whatsapp,
title: "whatsapp".localized,
subtitle: String(format: "whatsapp_phone_number".localized, phone),
icon: "message.circle"
)
}
// MARK: - Viber
private static func parseViber(_ content: String) -> ParsedQRData {
let phone = content.replacingOccurrences(of: "viber://add?number=", with: "", options: .caseInsensitive)
return ParsedQRData(
type: .viber,
title: "viber".localized,
subtitle: String(format: "viber_phone_number".localized, phone),
icon: "bubble.left.and.bubble.right"
)
}
// MARK: - Snapchat
private static func parseSnapchat(_ content: String) -> ParsedQRData {
let username = content.replacingOccurrences(of: "snapchat://", with: "", options: .caseInsensitive)
return ParsedQRData(
type: .snapchat,
title: "snapchat".localized,
subtitle: String(format: "snapchat_username".localized, username),
icon: "camera.viewfinder"
)
}
// MARK: - TikTok
private static func parseTikTok(_ content: String) -> ParsedQRData {
var username = ""
if content.lowercased().contains("www.tiktok.com") {
// https://www.tiktok.com/@username
if let atIndex = content.lastIndex(of: "@") {
username = String(content[content.index(after: atIndex)...])
}
} else if content.lowercased().contains("tiktok.com") {
// https://tiktok.com/@username
if let atIndex = content.lastIndex(of: "@") {
username = String(content[content.index(after: atIndex)...])
}
}
return ParsedQRData(
type: .tiktok,
title: "tiktok".localized,
subtitle: String(format: "tiktok_username".localized, username),
icon: "music.mic"
)
}
// MARK: - URL
private static func parseURL(_ content: String) -> ParsedQRData {
return ParsedQRData(
type: .url,
title: "url_link".localized,
subtitle: content,
icon: "link"
)
}
// MARK: - Location
private static func parseLocation(_ content: String) -> ParsedQRData {
let coordinates = content.replacingOccurrences(of: "geo:", with: "")
let coords = coordinates.components(separatedBy: ",")
let latitude = coords.first ?? ""
let longitude = coords.count > 1 ? coords[1] : ""
let title = "geolocation".localized
let subtitle = String(format: "geolocation_coordinates".localized, latitude, longitude)
return ParsedQRData(
type: .location,
title: title,
subtitle: subtitle,
icon: "location"
)
}
// MARK: -
/// vCard
static func parseContactInfoFromVCard(_ content: String) -> ContactInfo {
let lines = content.components(separatedBy: .newlines)
var contactInfo = ContactInfo()
for line in lines {
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
if trimmedLine.hasPrefix("FN:") {
contactInfo.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 ?? ""
contactInfo.phoneNumber = number
}
} else if trimmedLine.hasPrefix("EMAIL") {
let emailValue = String(trimmedLine.dropFirst(5))
if emailValue.contains(":") {
let emailAddress = emailValue.components(separatedBy: ":").last ?? ""
contactInfo.email = emailAddress
}
} else if trimmedLine.hasPrefix("ORG:") {
contactInfo.organization = String(trimmedLine.dropFirst(4))
} else if trimmedLine.hasPrefix("TITLE:") {
contactInfo.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 {
contactInfo.address = "\(addressComponents[2]) \(addressComponents[1])"
}
}
}
}
}
return contactInfo
}
/// MeCard
static func parseContactInfoFromMeCard(_ content: String) -> ContactInfo {
let mecardInfo = content.replacingOccurrences(of: "MECARD:", with: "")
let components = mecardInfo.components(separatedBy: ";")
var contactInfo = ContactInfo()
for component in components {
let trimmedComponent = component.trimmingCharacters(in: .whitespaces)
if trimmedComponent.isEmpty { continue }
if trimmedComponent.hasPrefix("N:") {
let nameValue = String(trimmedComponent.dropFirst(2))
let nameParts = nameValue.components(separatedBy: ",")
if nameParts.count >= 2 {
let lastName = nameParts[0]
let firstName = nameParts[1]
contactInfo.name = "\(firstName) \(lastName)"
} else if nameParts.count == 1 {
contactInfo.name = nameParts[0]
}
} else if trimmedComponent.hasPrefix("TEL:") {
contactInfo.phoneNumber = String(trimmedComponent.dropFirst(4))
} else if trimmedComponent.hasPrefix("EMAIL:") {
contactInfo.email = String(trimmedComponent.dropFirst(6))
} else if trimmedComponent.hasPrefix("ORG:") {
contactInfo.organization = String(trimmedComponent.dropFirst(4))
} else if trimmedComponent.hasPrefix("TITLE:") {
contactInfo.title = String(trimmedComponent.dropFirst(6))
} else if trimmedComponent.hasPrefix("ADR:") {
contactInfo.address = String(trimmedComponent.dropFirst(4))
}
}
return contactInfo
}
///
static func parseContactInfo(from content: String) -> ContactInfo? {
if content.hasPrefix("BEGIN:VCARD") {
let normalizedVCard = VCardConverter.normalizeVCard(content)
let contactInfo = parseContactInfoFromVCard(normalizedVCard)
return (contactInfo.name.isEmpty && contactInfo.phoneNumber.isEmpty) ? nil : contactInfo
} else if content.hasPrefix("MECARD:") {
let contactInfo = parseContactInfoFromMeCard(content)
return (contactInfo.name.isEmpty && contactInfo.phoneNumber.isEmpty) ? nil : contactInfo
}
return nil
}
// MARK: - URL
private static func isValidURL(_ string: String) -> Bool {
guard let url = URL(string: string) else { return false }
return UIApplication.shared.canOpenURL(url)
}
}