contacts-framework
dpearson2699/swift-ios-skills
Read, create, update, and pick contacts using iOS Contacts and ContactsUI frameworks.
What is contacts-framework?
Provides access to the Contacts framework for fetching, creating, updating, and deleting contacts in Swift iOS apps. Use this when you need to work with CNContactStore, handle contact permissions, integrate contact pickers, or manage contact data with CNSaveRequest.
- Fetch contacts by name, identifier, or enumerate all contacts with configurable key descriptors
- Create new contacts and update existing contacts using CNMutableContact and CNSaveRequest
- Delete contacts from the address book
- Wrap CNContactPickerViewController for contact selection in SwiftUI
- Handle contact authorization states including limited access (iOS 18+)
- Observe contact store changes and manage contact permissions
How to install contacts-framework
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill contacts-framework- Swift 6.3 or later
- iOS 18+ (or compatible iOS version)
- NSContactsUsageDescription key in Info.plist explaining contact access
- com.apple.developer.contacts.notes entitlement only if reading/writing contact notes
How to use contacts-framework
- 1.Add NSContactsUsageDescription to Info.plist with a user-facing reason for contact access
- 2.Import Contacts and ContactsUI frameworks in your Swift files
- 3.Request contact authorization using CNContactStore.requestAccess(for:) before fetching or saving
- 4.Define key descriptors for the contact properties you need to fetch (e.g., CNContactGivenNameKey, CNContactPhoneNumbersKey)
- 5.Use unifiedContacts(matching:keysToFetch:) for predicate-based queries or enumerateContacts(with:usingBlock:) for batch enumeration
- 6.Create or update contacts using CNMutableContact and persist changes with CNSaveRequest.execute()
- 7.Wrap CNContactPickerViewController in a UIViewControllerRepresentable for SwiftUI integration
Use cases
- Building a messaging or calling app that needs to access user contacts
- Creating a contact management or CRM application with create/edit/delete functionality
- Implementing a contact picker UI for selecting recipients in an email or messaging feature
- Syncing app-specific contact data with the system address book
- Displaying contact photos and detailed information in a custom contact view
- iOS app developers building contact-dependent features
- SwiftUI developers integrating native contact pickers
- Backend developers managing contact synchronization
- Apps requiring address book read/write access
contacts-framework FAQ
It throws CNContactPropertyNotFetchedException. Always include the properties you need in the keysToFetch array.
No, ordinary contact read/write access requires only NSContactsUsageDescription in Info.plist. The com.apple.developer.contacts.notes entitlement is only needed if reading or writing the contact note field.
With .limited authorization, fetch and edit operations only apply to contacts the user granted access to or the app created. Use ContactAccessButton to let users add more contacts to the app's limited-access set.
No, the picker does not require authorization. The system grants access only to the contacts the user selects in the picker.
If CNSaveRequest.execute() throws, surface the error, keep the unsaved intent available to the user, correct the known cause (authorization, read-only container, or invalid input), and retry with a fresh request.
Full instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
name: contacts-framework description: "Read, create, update, and pick contacts using the Contacts and ContactsUI frameworks. Use when fetching contact data, saving new contacts, wrapping CNContactPickerViewController in SwiftUI, handling contact permissions, or working with CNContactStore fetch and save requests."
Contacts Framework
Use CNContactStore, CNSaveRequest, and CNContactPickerViewController to
fetch, create, update, or pick contacts in Swift 6.3 / iOS 26+ apps.
Contents
- Setup
- Authorization
- Fetching Contacts
- Key Descriptors
- Creating and Updating Contacts
- Contact Picker
- Observing Changes
- Common Mistakes
- Review Checklist
- References
Setup
Project Configuration
- Add
NSContactsUsageDescriptionto Info.plist explaining why the app accesses contacts. The app crashes if it uses contact data APIs without this key. - No additional capability or entitlement is required for ordinary Contacts access.
- Add
com.apple.developer.contacts.notesonly when reading or writingCNContactNoteKey/CNContact.note; this entitlement requires Apple approval before public distribution.
Imports
@preconcurrency import Contacts // CNContactStore, CNSaveRequest, CNContact
import ContactsUI // CNContactPickerViewController
Authorization
Request access before fetching or saving contacts. The picker (CNContactPickerViewController)
does not require authorization -- the system grants access only to the contacts
the user selects.
let store = CNContactStore()
func requestAccess() async throws -> Bool {
return try await store.requestAccess(for: .contacts)
}
// Check current status without prompting
func checkStatus() -> CNAuthorizationStatus {
CNContactStore.authorizationStatus(for: .contacts)
}
Authorization States
| Status | Meaning |
|---|---|
.notDetermined | User has not been prompted yet |
.authorized | Full read/write access granted |
.denied | User denied access; direct to Settings |
.restricted | Parental controls or MDM restrict access |
.limited | iOS 18+: user granted access to selected contacts only |
Treat both .authorized and .limited as usable Contacts API states. With
.limited, fetch, edit, and delete operations only apply to contacts the user
granted or the app created. Use ContactAccessButton or
contactAccessPicker(isPresented:completionHandler:) to let users add contacts
to the app's limited-access set.
Fetching Contacts
Use unifiedContacts(matching:keysToFetch:) for predicate-based queries.
Use enumerateContacts(with:usingBlock:) for batch enumeration of all contacts.
For large cached address books, first fetch identifiers, then fetch detailed
contacts in batches by identifier.
Fetch by Name
func fetchContacts(named name: String) throws -> [CNContact] {
let predicate = CNContact.predicateForContacts(matchingName: name)
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
return try store.unifiedContacts(matching: predicate, keysToFetch: keys)
}
Fetch by Identifier
func fetchContact(identifier: String) throws -> CNContact {
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor
]
return try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys)
}
Enumerate All Contacts
Perform I/O-heavy enumeration off the main thread.
func fetchAllContacts() throws -> [CNContact] {
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor
]
let request = CNContactFetchRequest(keysToFetch: keys)
request.sortOrder = .givenName
var contacts: [CNContact] = []
try store.enumerateContacts(with: request) { contact, _ in
contacts.append(contact)
}
return contacts
}
Key Descriptors
Only fetch the properties you need. Accessing an unfetched property throws
CNContactPropertyNotFetchedException.
Common Keys
| Key | Property |
|---|---|
CNContactGivenNameKey | First name |
CNContactFamilyNameKey | Last name |
CNContactPhoneNumbersKey | Phone numbers array |
CNContactEmailAddressesKey | Email addresses array |
CNContactPostalAddressesKey | Mailing addresses array |
CNContactImageDataKey | Full-resolution contact photo |
CNContactThumbnailImageDataKey | Thumbnail contact photo |
CNContactBirthdayKey | Birthday date components |
CNContactOrganizationNameKey | Company name |
Composite Key Descriptors
Use CNContactFormatter.descriptorForRequiredKeys(for:) to fetch all keys needed
for formatting a contact's name.
let nameKeys = CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
let keys: [CNKeyDescriptor] = [nameKeys, CNContactPhoneNumbersKey as CNKeyDescriptor]
Creating and Updating Contacts
Use CNMutableContact to build new contacts and CNSaveRequest to persist changes.
Creating a New Contact
func createContact(givenName: String, familyName: String, phone: String) throws {
let contact = CNMutableContact()
contact.givenName = givenName
contact.familyName = familyName
contact.phoneNumbers = [
CNLabeledValue(
label: CNLabelPhoneNumberMobile,
value: CNPhoneNumber(stringValue: phone)
)
]
let saveRequest = CNSaveRequest()
saveRequest.add(contact, toContainerWithIdentifier: nil) // nil = default container
try store.execute(saveRequest)
}
Updating an Existing Contact
You must fetch the contact with the properties you intend to modify, create a mutable copy, change the properties, then save.
func updateContactEmail(identifier: String, email: String) throws {
let keys: [CNKeyDescriptor] = [
CNContactEmailAddressesKey as CNKeyDescriptor
]
let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys)
guard let mutable = contact.mutableCopy() as? CNMutableContact else { return }
mutable.emailAddresses.append(
CNLabeledValue(label: CNLabelWork, value: email as NSString)
)
let saveRequest = CNSaveRequest()
saveRequest.update(mutable)
try store.execute(saveRequest)
}
Deleting a Contact
func deleteContact(identifier: String) throws {
let keys: [CNKeyDescriptor] = [CNContactIdentifierKey as CNKeyDescriptor]
let contact = try store.unifiedContact(withIdentifier: identifier, keysToFetch: keys)
guard let mutable = contact.mutableCopy() as? CNMutableContact else { return }
let saveRequest = CNSaveRequest()
saveRequest.delete(mutable)
try store.execute(saveRequest)
}
Save Result and Recovery
try store.execute(saveRequest) returning without throwing is the save success
checkpoint. Update an app-side cache or success UI only after that return. If it
throws, surface or propagate the error, keep the unsaved intent available to the
user, and correct the known cause—such as authorization, a read-only container,
or invalid input—before building a fresh request. Serialize overlapping saves,
do not access a request while execute(_:) is using it, and refetch a possibly
stale contact before a corrected retry when access permits. Do not blindly
repeat the same destructive request or require a universal read-back that the
current access level may not permit. Load
Extended Contacts Patterns for multi-select,
vCard, and optimized-search workflows.
Contact Picker
CNContactPickerViewController lets users pick contacts without granting full
Contacts access. The app receives only the selected contact data.
SwiftUI Wrapper
import SwiftUI
import ContactsUI
struct ContactPicker: UIViewControllerRepresentable {
@Binding var selectedContact: CNContact?
func makeUIViewController(context: Context) -> CNContactPickerViewController {
let picker = CNContactPickerViewController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, CNContactPickerDelegate {
let parent: ContactPicker
init(_ parent: ContactPicker) {
self.parent = parent
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
parent.selectedContact = contact
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
parent.selectedContact = nil
}
}
}
Using the Picker
struct ContactSelectionView: View {
@State private var selectedContact: CNContact?
@State private var showPicker = false
var body: some View {
VStack {
if let contact = selectedContact {
Text("\(contact.givenName) \(contact.familyName)")
}
Button("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker(selectedContact: $selectedContact)
}
}
}
Filtering the Picker
Use predicates to control which contacts appear and what the user can select.
let picker = CNContactPickerViewController()
// Only show contacts that have an email address
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
// Selecting a contact returns it directly (no detail card)
picker.predicateForSelectionOfContact = NSPredicate(value: true)
Observing Changes
Listen for external contact database changes to refresh cached data.
func observeContactChanges() {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
// Refetch contacts -- cached CNContact objects are stale
refreshContacts()
}
}
Common Mistakes
DON'T: Fetch all keys when you only need a name
Over-fetching wastes memory and slows queries, especially for contacts with large photos.
// WRONG: Fetches far more than the UI displays, including full-resolution photos
let keys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor
]
// CORRECT: Fetch only what you display
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor
]
DON'T: Access unfetched properties
Accessing a property that was not in keysToFetch throws
CNContactPropertyNotFetchedException at runtime.
// WRONG: Only fetched name keys, now accessing phone
let keys: [CNKeyDescriptor] = [CNContactGivenNameKey as CNKeyDescriptor]
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
let phone = contact.phoneNumbers.first // CRASH
// CORRECT: Include the key you need
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
DON'T: Mutate a CNContact directly
CNContact is immutable. You must call mutableCopy() to get a CNMutableContact.
// WRONG: CNContact has no setter
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
contact.givenName = "New Name" // Compile error
// CORRECT: Create mutable copy
guard let mutable = contact.mutableCopy() as? CNMutableContact else { return }
mutable.givenName = "New Name"
DON'T: Skip authorization and assume access
Do not let fetch or save calls be the first place the user sees authorization.
If status is .notDetermined, request access; if access was denied, contact
operations fail with an authorization error.
// WRONG: Jump straight to fetch
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
// CORRECT: Check or request access first
let granted = try await store.requestAccess(for: .contacts)
guard granted else { return }
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
DON'T: Run heavy fetches on the main thread
enumerateContacts performs I/O. Running it on the main thread blocks the UI.
When strict concurrency checks complain about CNContact crossing task or actor
boundaries, use @preconcurrency import Contacts in that file or map contacts
into Sendable view models before returning them.
// WRONG: Main thread enumeration
func loadContacts() {
try store.enumerateContacts(with: request) { contact, _ in ... }
}
// CORRECT: Run on a background thread
func loadContacts() async throws -> [CNContact] {
try await Task.detached {
var results: [CNContact] = []
try store.enumerateContacts(with: request) { contact, _ in
results.append(contact)
}
return results
}.value
}
Review Checklist
-
NSContactsUsageDescriptionadded to Info.plist -
requestAccess(for: .contacts)called before fetch or save operations -
.limitedtreated as usable access with selected-contact caveats -
ContactAccessButtonorcontactAccessPickeroffered when users need to expand limited access - Authorization denial handled gracefully (guide user to Settings)
- Only needed
CNKeyDescriptorkeys included in fetch requests -
CNContactFormatter.descriptorForRequiredKeys(for:)used when formatting names - Mutable copy created via
mutableCopy()before modifying contacts - Every create/update/delete uses
CNSaveRequest; app state advances only afterexecute(_:)succeeds, and failures are surfaced before a corrected request is constructed - Heavy fetches (
enumerateContacts) run off the main thread -
CNContactStoreDidChangeobserved to refresh cached contacts -
CNContactPickerViewControllerused when full Contacts access is unnecessary - Picker predicates set before presenting the picker view controller
- Single
CNContactStoreinstance reused across the app
References
- Extended patterns (multi-select picker, vCard export, search optimization): references/contacts-patterns.md
- Contacts framework
- CNContactStore
- CNContactFetchRequest
- CNSaveRequest
- CNMutableContact
- CNContactPickerViewController
- CNContactPickerDelegate
- Accessing the contact store
- NSContactsUsageDescription
- ContactAccessButton
- contactAccessPicker(isPresented:completionHandler:)
- Contact Keys
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.

core-bluetooth
Build Bluetooth Low Energy central and peripheral workflows with Core Bluetooth scanning, connecting, and GATT communication.

core-data
Build and maintain Core Data persistence for apps not using SwiftData, with concurrency, batch operations, and schema migration.

core-motion
Access iOS device motion sensors: accelerometer, gyroscope, magnetometer, pedometer, activity recognition, altitude, and more.

core-nfc
Read and write NFC tags on iPhone using CoreNFC framework.

coreml
Load, configure, and run Core ML models in iOS apps for on-device machine learning inference.

cryptokit
Apple CryptoKit for Swift cryptographic primitives: hashing, HMAC, AES-GCM, signing, key agreement, and post-quantum operations.