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