0

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.

burnsi
  • 6,194
  • 13
  • 17
  • 27
Philippe
  • 5
  • 4
  • You forgot `.self`, `decodedContact.decode([String:String].self, ...` or is that only in the question? – Joakim Danielson Jan 29 '23 at 11:51
  • Yes, correct. I tired earlier, but it was giving an error, not the case now, so I assume I made another mistake while testing. But still, it does not then convert to [CNLabeledValue] – Philippe Jan 29 '23 at 12:02
  • What error do you get? – Joakim Danielson Jan 29 '23 at 12:09
  • I found my error, I was writing [String:String].Self, instead of .self... But then how do I convert to my [String:String] to [CNLabeledValue]? – Philippe Jan 29 '23 at 12:12
  • [this question](https://stackoverflow.com/questions/45622948/how-can-i-create-a-cncontact-with-a-cnpostaladdress) might be of help – Joakim Danielson Jan 29 '23 at 12:33
  • I am not sure how, for now my postalAddresses is a type [String:String], an array of key-value, not anything linked to CNPostalAddress. – Philippe Jan 29 '23 at 14:06

2 Answers2

0

I finally changed my mind as more issue where coming along and I created a structure that copy the CNMutableContact and two functions to convert from and to CNMutableContact. You will find the full implementation below.

Feel free to comment, I am still a newbie in Swift

//
//  ContactDetails.swift
//  ShareContacts
//
//  Created by Philippe on 04.02.23.
//

import Foundation
import Contacts

struct ContactDetails: Codable {
    var identifier: String
    
    var contactType: Int
    
    var namePrefix: String
    var givenName: String
    var middleName: String
    var familyName: String
    var previousFamilyName: String
    var nameSuffix: String
    var nickname: String
    
    var jobTitle: String
    var departmentName: String
    var organizationName: String
    
    var birthday: DateComponents?
    
    var note: String
    
    var postalAddresses: [PostalAddress]
    var emailAddresses: [LabeledValue]
    var urlAddresses: [LabeledValue]
    var phoneNumbers: [LabeledValue]
    var socialProfiles: [SocialProfile]
    
    init(identifier: String = "", contactType: Int = 0, namePrefix: String  = "", givenName: String  = "", middleName: String  = "", familyName: String = "", previousFamilyName: String = "", nameSuffix: String = "", nickname: String = "", jobTitle: String = "", departmentName: String = "", organizationName: String = "", birthday: DateComponents = DateComponents(), note: String = "", postalAddresses: [PostalAddress] = [], emailAddresses: [LabeledValue] = [], urlAddresses: [LabeledValue] = [], phoneNumbers: [LabeledValue] = [], socialProfiles: [SocialProfile] = []) {
        self.identifier = identifier
        self.contactType = contactType
        self.namePrefix = namePrefix
        self.givenName = givenName
        self.middleName = middleName
        self.familyName = familyName
        self.previousFamilyName = previousFamilyName
        self.nameSuffix = nameSuffix
        self.nickname = nickname
        self.jobTitle = jobTitle
        self.departmentName = departmentName
        self.organizationName = organizationName
        self.birthday = birthday
        self.note = note
        self.postalAddresses = postalAddresses
        self.emailAddresses = emailAddresses
        self.urlAddresses = urlAddresses
        self.phoneNumbers = phoneNumbers
        self.socialProfiles = socialProfiles
    }
}

extension ContactDetails {
    func convertToCNMutableContact(contact: ContactDetails) -> CNMutableContact {
        let cnMutableContact = CNMutableContact()
        
        cnMutableContact.contactType = CNContactType(rawValue: contact.contactType) ?? CNContactType.person
        
        cnMutableContact.namePrefix = contact.namePrefix
        cnMutableContact.givenName = contact.givenName
        cnMutableContact.middleName = contact.middleName
        cnMutableContact.familyName = contact.familyName
        cnMutableContact.previousFamilyName = contact.previousFamilyName
        cnMutableContact.nameSuffix = contact.nameSuffix
        cnMutableContact.nickname = contact.nickname
        
        cnMutableContact.jobTitle = contact.jobTitle
        cnMutableContact.departmentName = contact.departmentName
        cnMutableContact.organizationName = contact.organizationName
        
        cnMutableContact.birthday = contact.birthday
        
        cnMutableContact.note = contact.note
        
        contact.postalAddresses.forEach { postalAddress in
            let cnPostalAddress = CNMutablePostalAddress()
            cnPostalAddress.street = postalAddress.street
            cnPostalAddress.city = postalAddress.city
            cnPostalAddress.postalCode = postalAddress.postalCode
            cnPostalAddress.state = postalAddress.state
            cnPostalAddress.country = postalAddress.country
            
            cnMutableContact.postalAddresses.append(CNLabeledValue<CNPostalAddress>(label: postalAddress.label, value: cnPostalAddress))
        }
        
        contact.emailAddresses.forEach { emailAddress in
            cnMutableContact.emailAddresses.append(CNLabeledValue<NSString>(label: emailAddress.label, value: emailAddress.value as NSString))
        }
        
        contact.urlAddresses.forEach { urlAddress in
            cnMutableContact.urlAddresses.append(CNLabeledValue<NSString>(label: urlAddress.label, value: urlAddress.value as NSString))
        }
        
        contact.phoneNumbers.forEach { phoneNumber in
            let cnPhoneNumber = CNPhoneNumber(stringValue: phoneNumber.value)
            
            cnMutableContact.phoneNumbers.append(CNLabeledValue<CNPhoneNumber>(label: phoneNumber.label, value: cnPhoneNumber))
        }
        
        contact.socialProfiles.forEach { socialProfile in
            let cnSocialProfile = CNSocialProfile(urlString: socialProfile.urlString, username: socialProfile.username, userIdentifier: socialProfile.userIdentifier, service: socialProfile.service)
            
            cnMutableContact.socialProfiles.append(CNLabeledValue<CNSocialProfile>(label: socialProfile.label, value: cnSocialProfile))
        }
        
        return cnMutableContact
    }
    
    func convertFromCNMutableContact(cnMutableContact: CNMutableContact) -> ContactDetails {
        var contact = ContactDetails()
        
        contact.identifier = cnMutableContact.identifier
        
        contact.contactType = cnMutableContact.contactType.rawValue
        
        contact.namePrefix = cnMutableContact.namePrefix
        contact.givenName = cnMutableContact.givenName
        contact.middleName = cnMutableContact.middleName
        contact.familyName = cnMutableContact.familyName
        contact.previousFamilyName = cnMutableContact.previousFamilyName
        contact.nameSuffix = cnMutableContact.nameSuffix
        contact.nickname = cnMutableContact.nickname
        
        contact.jobTitle = cnMutableContact.jobTitle
        contact.departmentName = cnMutableContact.departmentName
        contact.organizationName = cnMutableContact.organizationName
        
        contact.birthday = cnMutableContact.birthday
        
        contact.note = cnMutableContact.note
        
        cnMutableContact.postalAddresses.forEach { cnPostalAddress in
            let postalAddress = PostalAddress(label: cnPostalAddress.label ?? "", street: cnPostalAddress.value.street, city: cnPostalAddress.value.city, postalCode: cnPostalAddress.value.postalCode, state: cnPostalAddress.value.state, country: cnPostalAddress.value.country)
            
            contact.postalAddresses.append(postalAddress)
        }
        
        cnMutableContact.emailAddresses.forEach  { emailAddress in
            contact.emailAddresses.append(LabeledValue(label: (emailAddress.label ?? "") as String, value: emailAddress.value as String))
        }
        
        cnMutableContact.urlAddresses.forEach  { urlAddress in
            contact.urlAddresses.append(LabeledValue(label: (urlAddress.label ?? "") as String, value: urlAddress.value as String))
        }
        
        cnMutableContact.phoneNumbers.forEach  { phoneNumber in
            contact.phoneNumbers.append(LabeledValue(label: (phoneNumber.label ?? "") as String, value: phoneNumber.value.stringValue))
        }
        
        cnMutableContact.socialProfiles.forEach { cnSocialProfile in
            let socialProfile = SocialProfile(label: cnSocialProfile.label ?? "", username: cnSocialProfile.value.username, service: cnSocialProfile.value.service, urlString: cnSocialProfile.value.urlString, userIdentifier: cnSocialProfile.value.userIdentifier)
            
            contact.socialProfiles.append(socialProfile)
        }
        
        return contact
    }
}
Philippe
  • 5
  • 4
0

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
}
vadian
  • 274,689
  • 30
  • 353
  • 361