Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
24930e6c3e
commit
1080590fde
@ -227,7 +227,7 @@ extension Contact {
|
||||
|
||||
let postalAddress = parseVCardADR(addressPart)
|
||||
if postalAddress.hasValue {
|
||||
let field = ContactField(typeId: "address", value: postalAddress.toJSON(), title: typeLabel, orderIndex: fieldIndex)
|
||||
let field = ContactField(typeId: "address", value: postalAddress.encode(), title: typeLabel, orderIndex: fieldIndex)
|
||||
field.contact = contact
|
||||
contact.contactFields?.append(field)
|
||||
fieldIndex += 1
|
||||
|
||||
@ -98,7 +98,7 @@ final class ContactField {
|
||||
guard typeId == "address" else { return nil }
|
||||
|
||||
// Parse as structured PostalAddress
|
||||
if let address = PostalAddress.fromJSON(value), address.hasValue {
|
||||
if let address = PostalAddress.decode(from: value), address.hasValue {
|
||||
return address
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ final class ContactField {
|
||||
/// Sets the postal address (stores as JSON)
|
||||
@MainActor
|
||||
func setPostalAddress(_ address: PostalAddress) {
|
||||
self.value = address.toJSON()
|
||||
self.value = address.encode()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -193,7 +193,7 @@ extension ContactFieldType {
|
||||
urlBuilder: { value in
|
||||
// Try to parse as PostalAddress JSON first for proper formatting
|
||||
let searchQuery: String
|
||||
if let address = PostalAddress.fromJSON(value) {
|
||||
if let address = PostalAddress.decode(from: value) {
|
||||
searchQuery = address.singleLineString
|
||||
} else {
|
||||
searchQuery = value
|
||||
@ -668,7 +668,7 @@ nonisolated private func formatAddressForDisplay(_ value: String) -> String {
|
||||
guard !trimmed.isEmpty else { return value }
|
||||
|
||||
// Parse as structured PostalAddress
|
||||
if let address = PostalAddress.fromJSON(trimmed), address.hasValue {
|
||||
if let address = PostalAddress.decode(from: trimmed), address.hasValue {
|
||||
return address.formattedString
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
/// A structured postal address for VCard compatibility and locale-aware formatting
|
||||
/// This struct is Sendable and all properties/methods are explicitly nonisolated for use in any context
|
||||
struct PostalAddress: Hashable, Sendable {
|
||||
/// Uses @preconcurrency Codable to allow nonisolated encoding/decoding in Swift 6
|
||||
struct PostalAddress: Hashable, Sendable, @preconcurrency Codable {
|
||||
var street: String = "" // Street address line 1 (can include apt/suite)
|
||||
var street2: String = "" // Street address line 2 (optional)
|
||||
var city: String = ""
|
||||
@ -74,23 +74,39 @@ extension PostalAddress {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - JSON Serialization
|
||||
// MARK: - String Serialization
|
||||
|
||||
extension PostalAddress {
|
||||
/// Encodes to pipe-delimited string for storage
|
||||
nonisolated func toJSON() -> String {
|
||||
// Use pipe delimiter since it's unlikely to appear in address fields
|
||||
[street, street2, city, state, postalCode, country].joined(separator: "|")
|
||||
/// Encodes to JSON string for storage in ContactField.value
|
||||
nonisolated func encode() -> String {
|
||||
guard let data = try? JSONEncoder().encode(self),
|
||||
let jsonString = String(data: data, encoding: .utf8) else {
|
||||
return ""
|
||||
}
|
||||
return jsonString
|
||||
}
|
||||
|
||||
/// Decodes from pipe-delimited string
|
||||
nonisolated static func fromJSON(_ jsonString: String) -> PostalAddress? {
|
||||
guard !jsonString.isEmpty else { return nil }
|
||||
/// Decodes from JSON string (with backward compatibility for legacy pipe-delimited format)
|
||||
nonisolated static func decode(from string: String) -> PostalAddress? {
|
||||
guard !string.isEmpty else { return nil }
|
||||
|
||||
let components = jsonString.split(separator: "|", omittingEmptySubsequences: false).map(String.init)
|
||||
// Try JSON decoding first (new format)
|
||||
if let data = string.data(using: .utf8),
|
||||
let address = try? JSONDecoder().decode(PostalAddress.self, from: data),
|
||||
address.hasValue {
|
||||
return address
|
||||
}
|
||||
|
||||
// Fall back to legacy pipe-delimited format for migration
|
||||
return decodeLegacy(from: string)
|
||||
}
|
||||
|
||||
/// Decodes from legacy pipe-delimited format (for backward compatibility)
|
||||
nonisolated private static func decodeLegacy(from string: String) -> PostalAddress? {
|
||||
let components = string.split(separator: "|", omittingEmptySubsequences: false).map(String.init)
|
||||
guard components.count == 6 else { return nil }
|
||||
|
||||
return PostalAddress(
|
||||
let address = PostalAddress(
|
||||
street: components[0],
|
||||
street2: components[1],
|
||||
city: components[2],
|
||||
@ -98,6 +114,8 @@ extension PostalAddress {
|
||||
postalCode: components[4],
|
||||
country: components[5]
|
||||
)
|
||||
|
||||
return address.hasValue ? address : nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,6 +43,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"123 Main St" : {
|
||||
|
||||
},
|
||||
"12345" : {
|
||||
|
||||
},
|
||||
"About" : {
|
||||
|
||||
@ -84,6 +90,9 @@
|
||||
},
|
||||
"Address" : {
|
||||
|
||||
},
|
||||
"Apt, Suite, Unit (optional)" : {
|
||||
|
||||
},
|
||||
"Are you sure you want to delete this card? This action cannot be undone." : {
|
||||
"comment" : "An alert message displayed when the user attempts to delete a card. It confirms the action and warns that it cannot be undone.",
|
||||
@ -152,6 +161,9 @@
|
||||
},
|
||||
"Choose a card in the My Cards tab to start sharing." : {
|
||||
|
||||
},
|
||||
"City" : {
|
||||
|
||||
},
|
||||
"Company" : {
|
||||
|
||||
@ -167,6 +179,12 @@
|
||||
},
|
||||
"Contact Fields" : {
|
||||
|
||||
},
|
||||
"Country" : {
|
||||
|
||||
},
|
||||
"Country (optional)" : {
|
||||
|
||||
},
|
||||
"Create multiple business cards" : {
|
||||
"extractionState" : "stale",
|
||||
@ -546,6 +564,15 @@
|
||||
},
|
||||
"Social Media" : {
|
||||
|
||||
},
|
||||
"State" : {
|
||||
|
||||
},
|
||||
"Street" : {
|
||||
|
||||
},
|
||||
"Street 2" : {
|
||||
|
||||
},
|
||||
"Suffix (e.g. Jr., III)" : {
|
||||
|
||||
@ -706,6 +733,9 @@
|
||||
},
|
||||
"Write down a memorable reminder about your contact" : {
|
||||
|
||||
},
|
||||
"ZIP Code" : {
|
||||
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
|
||||
@ -19,11 +19,14 @@ struct WatchSyncService {
|
||||
let email = card.firstContactField(ofType: "email")?.value ?? ""
|
||||
let phone = card.firstContactField(ofType: "phone")?.value ?? ""
|
||||
let website = card.firstContactField(ofType: "website")?.value ?? ""
|
||||
let location = card.firstContactField(ofType: "address")?.value ?? ""
|
||||
let linkedIn = card.firstContactField(ofType: "linkedIn")?.value ?? ""
|
||||
let twitter = card.firstContactField(ofType: "twitter")?.value ?? ""
|
||||
let instagram = card.firstContactField(ofType: "instagram")?.value ?? ""
|
||||
|
||||
// Format address for display (decode from stored JSON/legacy format)
|
||||
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
|
||||
let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue
|
||||
|
||||
return SyncableCard(
|
||||
id: card.id,
|
||||
displayName: card.displayName,
|
||||
|
||||
@ -257,7 +257,7 @@ private struct ContactFieldRowView: View {
|
||||
state: "TX",
|
||||
postalCode: "75024"
|
||||
)
|
||||
let addressField = ContactField(typeId: "address", value: address.toJSON(), title: "Work", orderIndex: 3)
|
||||
let addressField = ContactField(typeId: "address", value: address.encode(), title: "Work", orderIndex: 3)
|
||||
|
||||
let linkedInField = ContactField(typeId: "linkedIn", value: "linkedin.com/in/mattbruce", title: "", orderIndex: 4)
|
||||
let twitterField = ContactField(typeId: "twitter", value: "twitter.com/mattbruce", title: "", orderIndex: 5)
|
||||
|
||||
@ -28,7 +28,7 @@ struct AddedContactField: Identifiable, Equatable {
|
||||
var shortDisplayValue: String {
|
||||
if fieldType.id == "address" {
|
||||
// For addresses, show single-line format in the list
|
||||
if let address = PostalAddress.fromJSON(value), address.hasValue {
|
||||
if let address = PostalAddress.decode(from: value), address.hasValue {
|
||||
return address.singleLineString
|
||||
}
|
||||
}
|
||||
@ -190,7 +190,7 @@ private struct FieldRow: View {
|
||||
AddedContactField(fieldType: .email, value: "matt@example.com", title: "Work"),
|
||||
AddedContactField(fieldType: .email, value: "personal@example.com", title: "Personal"),
|
||||
AddedContactField(fieldType: .phone, value: "+1 (555) 123-4567", title: "Cell"),
|
||||
AddedContactField(fieldType: .address, value: address.toJSON(), title: "Work"),
|
||||
AddedContactField(fieldType: .address, value: address.encode(), title: "Work"),
|
||||
AddedContactField(fieldType: .linkedIn, value: "linkedin.com/in/mattbruce", title: "Connect with me")
|
||||
]
|
||||
}()
|
||||
|
||||
@ -32,7 +32,7 @@ struct ContactFieldEditorSheet: View {
|
||||
|
||||
// Parse postal address if this is an address field
|
||||
if fieldType.id == "address" {
|
||||
if let parsed = PostalAddress.fromJSON(initialValue), parsed.hasValue {
|
||||
if let parsed = PostalAddress.decode(from: initialValue), parsed.hasValue {
|
||||
_postalAddress = State(initialValue: parsed)
|
||||
} else {
|
||||
_postalAddress = State(initialValue: PostalAddress())
|
||||
@ -152,7 +152,7 @@ struct ContactFieldEditorSheet: View {
|
||||
Button("Save") {
|
||||
if isAddressField {
|
||||
// Save postal address as JSON
|
||||
onSave(postalAddress.toJSON(), title)
|
||||
onSave(postalAddress.encode(), title)
|
||||
} else {
|
||||
onSave(value.trimmingCharacters(in: .whitespacesAndNewlines), title)
|
||||
}
|
||||
@ -304,7 +304,7 @@ private struct FlowLayout: Layout {
|
||||
|
||||
ContactFieldEditorSheet(
|
||||
fieldType: .address,
|
||||
initialValue: existingAddress.toJSON(),
|
||||
initialValue: existingAddress.encode(),
|
||||
initialTitle: "Work"
|
||||
) { value, title in
|
||||
print("Saved: \(value), title: \(title)")
|
||||
|
||||
@ -19,13 +19,14 @@ struct BusinessCardTests {
|
||||
displayName: "Test User",
|
||||
role: "Developer",
|
||||
company: "Test Corp",
|
||||
email: "test@example.com",
|
||||
phone: "+1 555 123 4567",
|
||||
website: "example.com",
|
||||
location: "San Francisco, CA",
|
||||
bio: "A passionate developer"
|
||||
)
|
||||
context.insert(card)
|
||||
|
||||
// Add contact fields using the ContactField relationship
|
||||
card.addContactField(.email, value: "test@example.com", title: "Work")
|
||||
card.addContactField(.phone, value: "+1 555 123 4567", title: "Cell")
|
||||
card.addContactField(.website, value: "example.com", title: "")
|
||||
|
||||
#expect(card.vCardPayload.contains("BEGIN:VCARD"))
|
||||
#expect(card.vCardPayload.contains("FN:Test User"))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user