What can you do with the Contacts framework without permission, and how do formatters and vCards behave?
Predict first
What do the model and vCard round trip show?
import Foundation
import Contacts
let c = CNMutableContact()
c.givenName = "Asha"; c.familyName = "Rao"; c.organizationName = "Swift Bank"
c.phoneNumbers = [CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: "+91 98765-43210"))]
let home = CNMutablePostalAddress()
home.street = "1 MG Road"; home.city = "Pune"; home.postalCode = "411001"; home.country = "India"
c.postalAddresses = [CNLabeledValue(label: CNLabelHome, value: home)]
print("full name:", CNContactFormatter.string(from: c, style: .fullName) ?? "nil")
print("phone kept as typed:", c.phoneNumbers[0].value.stringValue)
print("label:", CNLabeledValue<NSString>.localizedString(forLabel: CNLabelPhoneNumberMobile))
print("address:", CNPostalAddressFormatter.string(from: home, style: .mailingAddress).replacingOccurrences(of: "\n", with: " | "))
let vcard = String(data: try! CNContactVCardSerialization.data(with: [c]), encoding: .utf8)!
for l in vcard.split(whereSeparator: \.isNewline) where l.hasPrefix("N:") || l.hasPrefix("FN:") || l.hasPrefix("TEL") || l.hasPrefix("ORG") { print("vcard:", l) }
let back = try! CNContactVCardSerialization.contacts(with: Data(vcard.utf8))[0]
print("round trip:", back.givenName, back.familyName, "same identifier:", back.identifier == c.identifier)
print("status without asking:", CNContactStore.authorizationStatus(for: .contacts).rawValue, "(0 = not determined)")
- The formatter gives “Asha Rao”, the vCard has N/FN/ORG/TEL lines, and the identifier differs after the round trip
- The identifier survives the round trip
- The status is authorized
Mental model. A contact card on your desk is just paper you can copy and print; the address book is the locked cabinet.
CNMutableContact and the value types around it (CNLabeledValue, CNPhoneNumber, CNMutablePostalAddress) are plain objects: you can build, format and serialise them with no authorization. In the run CNContactFormatter gives “Asha Rao”, the phone number keeps its typed string (CNPhoneNumber does not normalise; use a phone-number library or the string for display only), CNPostalAddressFormatter lays the address out as a mailing address, the status is 0 (not determined), and CNContactVCardSerialization writes N:, FN:, ORG and TEL;type=CELL lines. Importantly the identifier changes when a vCard is parsed back (a new contact object), so the identifier is a store-local key, not a portable id.
Reading the user’s contacts needs the permission and every key you touch must be in keysToFetch (CNContactFormatter.descriptorForRequiredKeys(for:) supplies the keys the formatter needs). Prefer CNContactPickerViewController, which needs no access. The macOS command-line run exercised only the in-memory model; fetching from a real store was not run (Q378 covers the access rules).
Contact objects work offline; ids are local to a store.
Common traps
- Treating
identifieras stable across exports. A vCard import creates a new one. - Parsing phone numbers by hand. Display the string, normalise with a proper library.
They might follow up with
- How do I share a contact? Serialise to a vCard and share the data.
Related questions
- 17.3.1 How do the Contacts, EventKit and Photos frameworks gate access, and how should an app handle limited or…
How is an EventKit event modelled: recurrence rules, alarms… →