import Foundation /// A typed value with a vCard `TYPE` label, e.g. a `WORK` phone number. public struct LabeledValue: Sendable, Equatable { /// Already normalised to a vCard TYPE token: `HOME`, `WORK`, `CELL`, … public var label: String public var value: String public init(label: String, value: String) { self.label = label self.value = value } } /// A postal address, held as the components vCard's `ADR` expects. public struct PostalAddress: Sendable, Equatable { public var label: String public var street: String public var city: String public var state: String public var postalCode: String public var country: String public init( label: String, street: String = "", city: String = "", state: String = "", postalCode: String = "", country: String = "" ) { self.label = label self.street = street self.city = city self.state = state self.postalCode = postalCode self.country = country } } /// A contact reduced to the fields a click-wheel iPod can actually display. /// /// Deliberately has no notes field. `CNContactNoteKey` has required the /// restricted `com.apple.developer.contacts.notes` entitlement since macOS 11, /// and fetching with it unentitled throws — so notes are never read. public struct IPodContact: Sendable, Equatable, Identifiable { public var id: String public var namePrefix: String public var givenName: String public var middleName: String public var familyName: String public var nameSuffix: String public var organization: String public var jobTitle: String public var phoneNumbers: [LabeledValue] public var emailAddresses: [LabeledValue] public var postalAddresses: [PostalAddress] public var birthday: DateComponents? public init( id: String = UUID().uuidString, namePrefix: String = "", givenName: String = "", middleName: String = "", familyName: String = "", nameSuffix: String = "", organization: String = "", jobTitle: String = "", phoneNumbers: [LabeledValue] = [], emailAddresses: [LabeledValue] = [], postalAddresses: [PostalAddress] = [], birthday: DateComponents? = nil ) { self.id = id self.namePrefix = namePrefix self.givenName = givenName self.middleName = middleName self.familyName = familyName self.nameSuffix = nameSuffix self.organization = organization self.jobTitle = jobTitle self.phoneNumbers = phoneNumbers self.emailAddresses = emailAddresses self.postalAddresses = postalAddresses self.birthday = birthday } /// The `FN` value, and the basis for the contact's filename. public var displayName: String { let parts = [namePrefix, givenName, middleName, familyName, nameSuffix] .filter { !$0.isEmpty } if !parts.isEmpty { return parts.joined(separator: " ") } if !organization.isEmpty { return organization } if let email = emailAddresses.first?.value, !email.isEmpty { return email } return "Contact" } /// True when the contact carries nothing worth putting on the device. public var isEmpty: Bool { phoneNumbers.isEmpty && emailAddresses.isEmpty && postalAddresses.isEmpty && organization.isEmpty && givenName.isEmpty && familyName.isEmpty } } /// A single dated occurrence, already expanded out of any recurrence rule. /// /// Times are absolute `Date`s here; they are rendered as *floating* local times /// at write time, because click-wheel iPods have no timezone database and /// reject or misread `TZID`-qualified values. public struct IPodEvent: Sendable, Equatable, Identifiable { public var id: String public var uid: String public var summary: String public var location: String public var notes: String public var start: Date public var end: Date public var isAllDay: Bool public var calendarTitle: String public init( id: String = UUID().uuidString, uid: String, summary: String, location: String = "", notes: String = "", start: Date, end: Date, isAllDay: Bool = false, calendarTitle: String = "" ) { self.id = id self.uid = uid self.summary = summary self.location = location self.notes = notes self.start = start self.end = end self.isAllDay = isAllDay self.calendarTitle = calendarTitle } }