What can you do with the Contacts framework without permission, and how do formatters and vCards behave?

17.10.1 · L3 · Interview

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 identifier as 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… →