Contacts Framework
Use CNContactStore, CNSaveRequest, and ContactsUI to fetch, mutate, or let
the user select contacts. Prefer the system picker when full address-book access
is unnecessary.
Contents
- Choose the access model
- Setup and authorization
- Fetch invariants
- Mutation invariants
- Concurrency and cache invalidation
- Common mistakes
- Review checklist
- References
Choose the access model
| Need | API |
|---|---|
| User chooses one or more contacts without broad permission | CNContactPickerViewController |
| App reads or writes its authorized contact set | CNContactStore |
| User expands an iOS 18+ limited set | ContactAccessButton or contactAccessPicker |
| Import/export or sharing | CNContactVCardSerialization |
Read Contacts extended patterns for a complete observable manager, SwiftUI lists, single/multi-select picker wrappers, email-only selection, optimized search, vCard import/export, groups, and change notification handling.
Setup and authorization
- Add
NSContactsUsageDescriptionbefore direct Contacts API access; missing it causes termination. - Ordinary access needs no entitlement. Reading or writing
CNContact.noterequires the Apple-approvedcom.apple.developer.contacts.notesentitlement. - The system contact picker does not require broad Contacts authorization; the app receives only selected data.
Treat authorization states explicitly:
| Status | Behavior |
|---|---|
.notDetermined |
Request only from a user-understood action |
.authorized |
Full access |
.limited |
Usable, but only for granted or app-created contacts |
.denied |
Explain the feature and route to Settings when appropriate |
.restricted |
Disable the operation; do not repeatedly prompt |
Fetch invariants
Only fetch keys the caller will access. Reading an unfetched property raises
CNContactPropertyNotFetchedException.
@preconcurrency import Contacts
let keys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor
]
let request = CNContactFetchRequest(keysToFetch: keys)
try store.enumerateContacts(with: request) { contact, stop in
consume(contact)
}
Use unifiedContacts(matching:keysToFetch:) for predicate queries,
unifiedContact(withIdentifier:keysToFetch:) for known identifiers, and
enumeration for the authorized address book. Avoid full-resolution image data
unless the UI truly requires it. For large caches, fetch identifiers first and
hydrate details in bounded batches.
Mutation invariants
- Create with
CNMutableContactandCNSaveRequest.add. - Update or delete by fetching the required properties, creating
mutableCopy(), then adding the operation to a fresh save request. store.execute(request)returning without throwing is the success boundary. Advance app state or clear drafts only afterward.- On failure, preserve the user's intent, surface the error, correct known authorization/container/input causes, refetch stale contacts when possible, and construct a new request. Do not blindly replay a destructive request.
- Serialize overlapping saves and never mutate a request while
executeuses it.
let mutable = CNMutableContact()
mutable.givenName = "Taylor"
let request = CNSaveRequest()
request.add(mutable, toContainerWithIdentifier: nil)
try store.execute(request)
Concurrency and cache invalidation
Enumeration is I/O-heavy; keep it off the main actor. With strict concurrency,
use @preconcurrency import Contacts only at the framework boundary or map
CNContact values into app-owned Sendable models before crossing actors.
Observe .CNContactStoreDidChange, invalidate cached CNContact objects, and
refetch the authorized set. Reuse one store instead of constructing stores per
row or query.
Common Mistakes
- Requesting full access when a picker satisfies the feature.
- Treating
.limitedas denial or assuming it exposes the full address book. - Fetching every key, especially full image data.
- Accessing a property not included in
keysToFetch. - Attempting to mutate immutable
CNContactdirectly. - Updating UI/cache before
executesucceeds. - Enumerating contacts on the main actor or retaining stale contact objects.
Review Checklist
- Usage description and note entitlement requirements are correct.
- Picker is preferred when broad access is unnecessary.
- Every authorization state, including
.limited, has product behavior. - Fetch descriptors include exactly the accessed properties.
- Name formatting uses the formatter's required-key descriptor.
- Create/update/delete use fresh
CNSaveRequestvalues and mutable contacts. - App state changes only after a successful save; failures preserve intent.
- Heavy reads run off the main actor and cross actors safely.
- Store-change notification invalidates and refetches caches.
- One long-lived
CNContactStoreis reused.
References
- Contacts extended patterns
- Contacts documentation
- CNContactStore
- CNSaveRequest
- CNContactPickerViewController
- Contact access controls