---
name: contacts-framework
description: "Contacts and ContactsUI: CNContactStore authorization and limited access, fetch and save requests, creating, updating and deleting contacts, CNContactPickerViewController in SwiftUI, change observation. Use when fetching, saving or picking contacts or handling permissions."
metadata:
  source: multi-agent-pipeline
---

# Contacts and ContactsUI

`CNContactStore` is the gateway to the user's address book: it answers
authorization questions, runs fetches and executes save requests.
ContactsUI adds the system contact picker and the SwiftUI controls for
limited access. Code here assumes Swift 6.3 and an iOS 26 deployment target.

Not for Swift change-history enumeration: the Contacts Swift API does not
offer it.

## Setup

- Info.plist must contain `NSContactsUsageDescription`. Calling the contact
  data APIs without it terminates the app.
- Ordinary Contacts access needs no capability or entitlement.
- The note field is special. Reading or writing `CNContactNoteKey`
  (`CNContact.note`) requires the `com.apple.developer.contacts.notes`
  entitlement, and Apple must approve that entitlement before the app can be
  distributed publicly.

```swift
@preconcurrency import Contacts   // store, contacts, save requests
import ContactsUI                 // picker and limited-access UI
```

## Authorization

Ask before the first fetch or save. The picker is the exception: it runs
out of process and hands over only what the user chose, so it needs no
permission at all.

```swift
let book = CNContactStore()   // create once, share across the app

func ensureAccess() async throws -> Bool {
    switch CNContactStore.authorizationStatus(for: .contacts) {
    case .authorized, .limited:
        return true
    case .notDetermined:
        return try await book.requestAccess(for: .contacts)
    case .denied, .restricted:
        return false          // explain and link to Settings
    @unknown default:
        return false
    }
}
```

`authorizationStatus(for:)` reads the state without prompting.

| Status | Meaning |
|---|---|
| `.notDetermined` | Never asked |
| `.authorized` | Full read and write access |
| `.limited` | iOS 18+: only the contacts the user shared with the app |
| `.denied` | User said no; point to Settings |
| `.restricted` | Blocked by parental controls or device management |

Treat `.authorized` and `.limited` alike as "can proceed". Under `.limited`,
fetches, edits and deletes only reach contacts the user granted plus contacts
the app created itself. Let people widen that set with `ContactAccessButton`
or the `contactAccessPicker(isPresented:completionHandler:)` modifier.

## Fetching

- `unifiedContacts(matching:keysToFetch:)` answers a predicate query.
- `unifiedContact(withIdentifier:keysToFetch:)` loads one known contact.
- `enumerateContacts(with:usingBlock:)` walks the whole book.

For big address books, collect identifiers first and then load details in
batches by identifier.

```swift
let wanted: [CNKeyDescriptor] = [
    CNContactGivenNameKey as CNKeyDescriptor,
    CNContactFamilyNameKey as CNKeyDescriptor,
    CNContactEmailAddressesKey as CNKeyDescriptor
]

let byName = try book.unifiedContacts(
    matching: CNContact.predicateForContacts(matchingName: "Okafor"),
    keysToFetch: wanted
)

let single = try book.unifiedContact(withIdentifier: savedID, keysToFetch: wanted)
```

Enumeration hits storage, so keep it off the main actor:

```swift
func everyone(in book: CNContactStore) async throws -> [CNContact] {
    try await Task.detached {
        let request = CNContactFetchRequest(keysToFetch: [
            CNContactGivenNameKey as CNKeyDescriptor,
            CNContactFamilyNameKey as CNKeyDescriptor
        ])
        request.sortOrder = .familyName
        var found: [CNContact] = []
        try book.enumerateContacts(with: request) { person, _ in found.append(person) }
        return found
    }.value
}
```

## Choosing keys

Ask only for the properties the screen shows. Reading a property that was
not fetched raises `CNContactPropertyNotFetchedException`.

| Key | Gives you |
|---|---|
| `CNContactGivenNameKey`, `CNContactFamilyNameKey` | Name parts |
| `CNContactPhoneNumbersKey` | Phone numbers |
| `CNContactEmailAddressesKey` | Email addresses |
| `CNContactPostalAddressesKey` | Postal addresses |
| `CNContactImageDataKey` | Full-resolution photo |
| `CNContactThumbnailImageDataKey` | Small photo for lists |
| `CNContactBirthdayKey` | Birthday as date components |
| `CNContactOrganizationNameKey` | Company |

To display a formatted name, let the formatter tell you what it needs and
mix that descriptor into the key list:

```swift
let listKeys: [CNKeyDescriptor] = [
    CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
    CNContactThumbnailImageDataKey as CNKeyDescriptor
]
```

## Writing

`CNContact` is immutable. Build or copy a `CNMutableContact`, describe the
change in a `CNSaveRequest`, then run it with `execute(_:)`.

### Create

```swift
let fresh = CNMutableContact()
fresh.givenName = "Imani"
fresh.familyName = "Okafor"
fresh.phoneNumbers = [
    CNLabeledValue(label: CNLabelPhoneNumberMobile,
                   value: CNPhoneNumber(stringValue: "+44 20 7946 0018"))
]

let create = CNSaveRequest()
create.add(fresh, toContainerWithIdentifier: nil)   // nil means the default container
try book.execute(create)
```

### Update

Fetch with every key you intend to change, copy, edit, save:

```swift
let current = try book.unifiedContact(
    withIdentifier: savedID,
    keysToFetch: [CNContactEmailAddressesKey as CNKeyDescriptor]
)
guard let editable = current.mutableCopy() as? CNMutableContact else { return }
editable.emailAddresses.append(
    CNLabeledValue(label: CNLabelWork, value: "imani@studio.example" as NSString)
)
let change = CNSaveRequest()
change.update(editable)
try book.execute(change)
```

### Delete

```swift
let target = try book.unifiedContact(
    withIdentifier: savedID,
    keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor]
)
if let doomed = target.mutableCopy() as? CNMutableContact {
    let removal = CNSaveRequest()
    removal.delete(doomed)
    try book.execute(removal)
}
```

## The contact picker

`CNContactPickerViewController` returns the chosen contact without the app
ever holding Contacts permission.

```swift
import ContactsUI
import SwiftUI

struct ContactChooser: UIViewControllerRepresentable {
    var onPick: (CNContact?) -> Void

    func makeUIViewController(context: Context) -> CNContactPickerViewController {
        let picker = CNContactPickerViewController()
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ picker: CNContactPickerViewController, context: Context) { }

    func makeCoordinator() -> Coordinator { Coordinator(onPick: onPick) }

    final class Coordinator: NSObject, CNContactPickerDelegate {
        let onPick: (CNContact?) -> Void
        init(onPick: @escaping (CNContact?) -> Void) { self.onPick = onPick }

        func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
            onPick(contact)
        }

        func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
            onPick(nil)
        }
    }
}

// .sheet(isPresented: $choosing) { ContactChooser { chosen = $0 } }
```

Set the picker's predicates before it is presented:

```swift
picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0")
picker.predicateForSelectionOfContact = NSPredicate(value: true)   // return at once, no detail card
```

## Watching for changes

Other apps and sync can change the address book at any time. Cached
`CNContact` values go stale, so listen for `.CNContactStoreDidChange` and
fetch again:

```swift
let token = NotificationCenter.default.addObserver(
    forName: .CNContactStoreDidChange, object: nil, queue: .main
) { _ in
    Task { @MainActor in await reloadVisibleContacts() }
}
```

## Common mistakes

- **Fetching too much.** Loading `CNContactImageDataKey`, postal addresses
  and birthdays for a list that shows names costs memory and time. Fetch the
  keys the screen uses.
- **Reading a key you did not fetch.** Asking for `phoneNumbers` after a
  name-only fetch crashes at runtime.
- **Mutating a `CNContact`.** It does not compile; take a `mutableCopy()`.
- **Letting a fetch be the first permission touchpoint.** Request access when
  the status is `.notDetermined`; otherwise the fetch simply fails with an
  authorization error.
- **Enumerating on the main actor.** It blocks the UI; use
  `Task.detached { ... }.value` or another background context.
- **Strict-concurrency errors about `CNContact` crossing actors.** Use
  `@preconcurrency import Contacts`, or map contacts into small `Sendable`
  view models before they leave the background task.

## Review checklist

- [ ] `NSContactsUsageDescription` present
- [ ] `requestAccess(for: .contacts)` happens before any fetch or save
- [ ] `.limited` handled as usable, with its narrower scope in mind
- [ ] `ContactAccessButton` or `contactAccessPicker` offered to grow limited access
- [ ] Denial handled with a path to Settings
- [ ] Only the needed keys fetched
- [ ] Names formatted with `descriptorForRequiredKeys(for:)` keys
- [ ] `mutableCopy()` taken before any change
- [ ] Every create, update and delete goes through `CNSaveRequest`
- [ ] Heavy fetches run off the main actor
- [ ] `CNContactStoreDidChange` observed
- [ ] Picker used whenever full access is not needed
- [ ] Picker predicates set before presentation
- [ ] One `CNContactStore` shared by the whole app

## References

- [contacts patterns](references/contacts-patterns.md): observable manager and list with limited access, multi-select and email pickers, predicates, vCard, groups, change-notification limits
- https://developer.apple.com/documentation/contacts
- https://developer.apple.com/documentation/contacts/cncontactstore
- https://developer.apple.com/documentation/contacts/cncontactfetchrequest
- https://developer.apple.com/documentation/contacts/cnsaverequest
- https://developer.apple.com/documentation/contacts/cnmutablecontact
- https://developer.apple.com/documentation/contacts/contact-keys
- https://developer.apple.com/documentation/contactsui/cncontactpickerviewcontroller
- https://developer.apple.com/documentation/contactsui/cncontactpickerdelegate
- https://developer.apple.com/documentation/contactsui/contactaccessbutton
- https://developer.apple.com/documentation/bundleresources/information-property-list/nscontactsusagedescription
- Apple article "Accessing the contact store" and the `contactAccessPicker(isPresented:completionHandler:)` modifier page in the ContactsUI documentation
