swiftcodablecncontact

Swift - how to Encode and Decode CNMutableContact array properly?


I am trying to make CNMutableContact "Codable". I have already built the encode function (see below), but I am getting some issues to decode array such as postalAddresses, emailAddresses, etc.

Here is my encode function:

public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    
    try container.encode(self.contact.contactType.rawValue, forKey: .contactType)
    
    try container.encode(self.contact.namePrefix, forKey: .namePrefix)
    try container.encode(self.contact.givenName, forKey: .givenName)
    try container.encode(self.contact.middleName, forKey: .middleName)
    try container.encode(self.contact.familyName, forKey: .familyName)
    try container.encode(self.contact.previousFamilyName, forKey: .previousFamilyName)
    try container.encode(self.contact.nameSuffix, forKey: .nameSuffix)
    try container.encode(self.contact.nickname, forKey: .nickname)
    
    try container.encode(self.contact.jobTitle, forKey: .jobTitle)
    try container.encode(self.contact.departmentName, forKey: .departmentName)
    try container.encode(self.contact.organizationName, forKey: .organizationName)
    
    var postalAddresses: [String:String] = [:]
    self.contact.postalAddresses.forEach { postalAddress in
        postalAddresses[postalAddress.label ?? "postal\(String(describing: index))"] = (CNPostalAddressFormatter.string(from: postalAddress.value, style: .mailingAddress))
    }
    try container.encode(postalAddresses, forKey: .postalAddresses)
    
    var emailAddresses: [String:String] = [:]
    self.contact.emailAddresses.forEach { emailAddress in
        emailAddresses[emailAddress.label ?? "email\(String(describing: index))"] = (emailAddress.value as String)
    }
    try container.encode(emailAddresses, forKey: .emailAddresses)
    
    var urlAddresses: [String:String] = [:]
    self.contact.urlAddresses.forEach { urlAddress in
        urlAddresses[urlAddress.label ?? "url\(String(describing: index))"] = (urlAddress.value as String)
    }
    try container.encode(urlAddresses, forKey: .urlAddresses)
    
    var phoneNumbers: [String:String] = [:]
    self.contact.phoneNumbers.forEach { phoneNumber in
        phoneNumbers[phoneNumber.label ?? "phone\(String(describing: index))"] = phoneNumber.value.stringValue
    }
    try container.encode(phoneNumbers, forKey: .phoneNumbers)
    
    var socialProfiles: [String:String] = [:]
    self.contact.socialProfiles.forEach { socialProfile in
        socialProfiles[socialProfile.label ?? "social\(String(describing: index))"] = socialProfile.value.urlString
    }
    try container.encode(socialProfiles, forKey: .socialProfiles)
    
    try container.encode(self.contact.birthday, forKey: .birthday)
    
    try container.encode(self.contact.note, forKey: .note)
}

As you can see, I encode the postalAddresses this way:

var postalAddresses: [String:String] = [:]
self.contact.postalAddresses.forEach { postalAddress in
      postalAddresses[postalAddress.label ?? "postal\(String(describing: index))"] = (CNPostalAddressFormatter.string(from: postalAddress.value, style: .mailingAddress))
}
try container.encode(postalAddresses, forKey: .postalAddresses)

But I have some difficulties to understand exactly how to decode it. Here is my decode function (not complete):

init(from decoder: Decoder) throws {
    let decodedContact = try decoder.container(keyedBy: CodingKeys.self)
    
    id = try decodedContact.decode(UUID.self, forKey: .id)
    contactIdentifier = try decodedContact.decode(String.self, forKey: .contactIdentifier)
    contact = CNMutableContact()
    
    var intContactType = try decodedContact.decode(Int.self, forKey: .contactType)
    if intContactType == 0 {
        contact.contactType = CNContactType.person
    } else {
        contact.contactType = CNContactType.organization
    }
    
    contact.namePrefix = try decodedContact.decode(String.self, forKey: .namePrefix)
    contact.givenName = try decodedContact.decode(String.self, forKey: .givenName)
    contact.middleName = try decodedContact.decode(String.self, forKey: .middleName)
    contact.familyName = try decodedContact.decode(String.self, forKey: .familyName)
    contact.previousFamilyName = try decodedContact.decode(String.self, forKey: .previousFamilyName)
    contact.nameSuffix = try decodedContact.decode(String.self, forKey: .nameSuffix)
    contact.nickname = try decodedContact.decode(String.self, forKey: .nickname)
    
    contact.jobTitle = try decodedContact.decode(String.self, forKey: .jobTitle)
    contact.departmentName = try decodedContact.decode(String.self, forKey: .departmentName)
    contact.organizationName = try decodedContact.decode(String.self, forKey: .organizationName)
    
    // MISSING ARRAYS
    let postalAddresses = try decodedContact.decode([String:String], forKey: .postalAddresses)
    
    contact.birthday = try decodedContact.decode(DateComponents.self, forKey: .birthday)
    
    contact.note = try decodedContact.decode(String.self, forKey: .note)
}

Note: the decode function returns an error with the postalAdresses decoding line.

Can you help me understand if my approach is correct and how to decode arrays?

Thanks

I have tried different ways to decode postalAddresses, but always getting an error.


Solution

  • It's not necessary to reinvent the wheel. CN(Mutable)Contact conforms to NSSecureCoding, it can be serialized to Data.

    And in Swift there is the PropertyWrapper pattern which exposes the instance and can perform the en-/decoding stuff under the hood

    @propertyWrapper
    struct CodableContact {
        var wrappedValue: CNMutableContact
    }
    
    extension CodableContact: Codable {
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let data = try container.decode(Data.self)
            guard let contact = try NSKeyedUnarchiver.unarchivedObject(ofClass: CNMutableContact.self, from: data) else {
                throw DecodingError.dataCorruptedError(
                    in: container,
                    debugDescription: "Invalid contact"
                )
            }
            wrappedValue = contact
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true)
            try container.encode(data)
        }
    }
    

    In your struct declare

    struct MyType: Codable {
        @CodableContact var contact: CNMutableContact
    }