
Contacts Framework
- 2.6k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
contacts-framework is an iOS skill for CNContactStore access, CRUD operations, and CNContactPickerViewController integration.
About
The contacts-framework skill documents iOS Contacts access with CNContactStore, fetch predicates, save requests, and CNContactPickerViewController. Setup requires NSContactsUsageDescription in Info.plist; missing keys crash on contact API use. The com.apple.developer.contacts.notes entitlement is needed only for note fields and requires Apple approval. Authorization uses requestAccess(for: .contacts) async and CNContactStore.authorizationStatus; the picker needs no authorization because users grant only selected contacts. iOS 18 limited access treats authorized and limited as usable; limited fetches apply only to allowed contacts, with ContactAccessButton to expand access. Fetching uses unifiedContacts for predicates, enumerateContacts for bulk reads off the main thread, and key descriptors to avoid CNContactPropertyNotFetchedException. Creating and updating flows copy mutable contacts after fetching intended keys, then execute CNSaveRequest add, update, or delete. SwiftUI ContactPicker wraps CNContactPickerViewController with a coordinator delegate. Common mistakes and a review checklist cover entitlement misuse, main-thread enumeration, and unfetched property access.
- NSContactsUsageDescription is required or contact APIs crash.
- Picker grants only selected contacts without full Contacts permission.
- iOS 18 limited access needs ContactAccessButton to add more contacts.
- Fetch only requested CNKeyDescriptor keys to avoid property exceptions.
- Use CNSaveRequest for create, update, and delete mutable contacts.
Contacts Framework by the numbers
- 2,587 all-time installs (skills.sh)
- +109 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #78 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
contacts-framework capabilities & compatibility
- Capabilities
- authorization and limited access handling on ios · predicate based fetch and batch enumeration patt · mutable contact create, update, and delete saves · swiftui contactpicker uiviewcontrollerrepresenta · key descriptor and entitlement guidance
- Use cases
- frontend · ui design
- Platforms
- macOS
What contacts-framework says it does
The app crashes if it uses contact data APIs without this key.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill contacts-frameworkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I safely read, write, and pick contacts with correct authorization and key fetching?
Access, create, update, and pick contacts with CNContactStore, CNContactPickerViewController, and proper iOS 18 limited authorization handling.
Who is it for?
iOS apps integrating address book pickers, sync, or contact editing features.
Skip if: Skip for Android contacts APIs or server-side CRM sync without on-device Contacts.framework.
When should I use this skill?
User mentions CNContactStore, contact picker, NSContactsUsageDescription, or limited contacts access.
What you get
Authorized Contacts flows with predicate fetches, save requests, and optional picker-only partial access.
- CNContactStore fetch code
- Contact picker SwiftUI wrapper
- Authorization and save-request handlers
By the numbers
- Targets Swift 6.3 and iOS 26+
Files
Contacts Framework
Fetch, create, update, and pick contacts from the user's Contacts database using CNContactStore, CNSaveRequest, and CNContactPickerViewController. Targets Swift 6.3 / iOS 26+.
Contents
- Setup
- Authorization
- Fetching Contacts
- Key Descriptors
- Creating and Updating Contacts
- Contact Picker
- Observing Changes
- Common Mistakes
- Review Checklist
- References
Setup
Project Configuration
1. Add NSContactsUsageDescription to Info.plist explaining why the app accesses contacts. The app crashes if it uses contact data APIs without this key. 2. No additional capability or entitlement is required for ordinary Contacts access. 3. Add com.apple.developer.contacts.notes only when reading or writing CNContactNoteKey / CNContact.note; this entitlement requires Apple approval before public distribution.
Imports
@preconcurrency import Contacts // CNContactStore, CNSaveRequest, CNContact
import ContactsUI // CNContactPickerViewControllerAuthorization
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)
}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 - [ ]
CNSaveRequestused for all create/update/delete operations - [ ] 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
{
"skill_name": "contacts-framework",
"evals": [
{
"id": 1,
"prompt": "I'm updating an iOS 26 SwiftUI contacts screen. It currently treats only CNAuthorizationStatus.authorized as usable and hides the list for .limited. Review the approach and show the authorization handling I should use, including how someone can add more contacts later.",
"expected_output": "Guidance that treats .authorized and .limited as usable Contacts API states, explains limited-access restrictions, and uses ContactsUI limited-access controls.",
"files": [],
"assertions": [
"Treats both .authorized and .limited as states where Contacts API fetch/save code can proceed.",
"Explains that .limited exposes only user-granted or app-created contacts, not the whole address book.",
"Recommends ContactAccessButton or contactAccessPicker for expanding limited access.",
"Distinguishes limited-access management from CNContactPickerViewController selection."
]
},
{
"id": 2,
"prompt": "Review this Contacts fetch plan: use CNContactCompleteNameKey for the displayed name, include CNContactImageDataKey by default for avatars, and fetch CNContactNoteKey so the app can show notes. What should change before I ship?",
"expected_output": "A source-grounded correction that uses valid name descriptors, avoids over-fetching images, and narrows the notes entitlement requirement.",
"files": [],
"assertions": [
"Rejects CNContactCompleteNameKey and uses CNContactFormatter.descriptorForRequiredKeys(for:) or valid individual name keys instead.",
"Recommends fetching only displayed fields and using thumbnail image data for list avatars instead of full image data by default.",
"States that CNContactNoteKey / CNContact.note requires the com.apple.developer.contacts.notes entitlement on modern Apple platforms.",
"Mentions that the contacts notes entitlement requires Apple approval before public App Store distribution."
]
},
{
"id": 3,
"prompt": "I want a no-permission contact picker for email selection and an incremental Contacts sync. The draft uses CNContactPickerViewController plus store.enumerateChanges(matching: CNChangeHistoryFetchRequest()). Review the API boundaries and give iOS Swift-safe guidance.",
"expected_output": "Guidance that keeps picker selection scoped to the user's final choice and rejects unsupported Swift change-history examples.",
"files": [],
"assertions": [
"States that CNContactPickerViewController does not require full Contacts authorization and returns only the user's final selection.",
"Mentions picker predicates such as predicateForEnablingContact or predicateForSelectionOfProperty must be configured before presentation.",
"Rejects store.enumerateChanges(matching:) as a nonexistent Contacts Swift API.",
"Explains that the change-history enumerator is Objective-C-only / unavailable in Swift and recommends CNContactStoreDidChange refetching or an explicit Objective-C bridge for true incremental history."
]
}
]
}
Contacts Framework Extended Patterns
Overflow reference for the contacts-framework skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- Contacts SwiftUI Integration
- Multi-Select Contact Picker
- Search and Filtering
- vCard Import and Export
- Contact Groups
- Change Notifications and Swift Boundaries
Contacts SwiftUI Integration
Contact Manager with @Observable
@preconcurrency import Contacts
import ContactsUI
import SwiftUI
import UIKit
@Observable
@MainActor
final class ContactManager {
let store = CNContactStore()
var contacts: [CNContact] = []
var canAccessContacts = false
var hasLimitedAccess = false
var authorizationStatus: CNAuthorizationStatus = .notDetermined
func checkAuthorization() {
updateAuthorization(CNContactStore.authorizationStatus(for: .contacts))
}
func requestAccess() async throws {
_ = try await store.requestAccess(for: .contacts)
updateAuthorization(CNContactStore.authorizationStatus(for: .contacts))
}
func updateAuthorization(_ status: CNAuthorizationStatus) {
authorizationStatus = status
canAccessContacts = status == .authorized || status == .limited
hasLimitedAccess = status == .limited
}
func loadContacts() async throws {
guard canAccessContacts else { return }
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactThumbnailImageDataKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
]
contacts = try await Task.detached { [store] in
let request = CNContactFetchRequest(keysToFetch: keys)
request.sortOrder = .givenName
var results: [CNContact] = []
try store.enumerateContacts(with: request) { contact, _ in
results.append(contact)
}
return results
}.value
}
func formattedName(for contact: CNContact) -> String {
CNContactFormatter.string(from: contact, style: .fullName)
?? "\(contact.givenName) \(contact.familyName)"
}
}Contact List View
struct ContactListView: View {
@Environment(ContactManager.self) private var manager
var body: some View {
NavigationStack {
Group {
if !manager.canAccessContacts {
ContentUnavailableView {
Label("Contacts Access", systemImage: "person.crop.circle.badge.questionmark")
} description: {
Text("Grant access to view your contacts.")
} actions: {
Button("Allow Access") {
Task { try? await manager.requestAccess() }
}
.buttonStyle(.borderedProminent)
}
} else {
contactList
if manager.hasLimitedAccess {
limitedAccessControls
}
}
}
.navigationTitle("Contacts")
.task {
manager.checkAuthorization()
if manager.canAccessContacts {
try? await manager.loadContacts()
}
}
}
}
private var contactList: some View {
List(manager.contacts, id: \.identifier) { contact in
HStack {
contactAvatar(contact)
VStack(alignment: .leading) {
Text(manager.formattedName(for: contact))
.font(.body)
if let phone = contact.phoneNumbers.first?.value.stringValue {
Text(phone)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
@ViewBuilder
private func contactAvatar(_ contact: CNContact) -> some View {
if let imageData = contact.thumbnailImageData,
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 40, height: 40)
.clipShape(Circle())
} else {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 40, height: 40)
.foregroundStyle(.secondary)
}
}
@State private var isPresentingContactAccessPicker = false
private var limitedAccessControls: some View {
Button {
isPresentingContactAccessPicker = true
} label: {
Label("Add Contacts", systemImage: "person.crop.circle.badge.plus")
}
.contactAccessPicker(isPresented: $isPresentingContactAccessPicker) { identifiers in
guard !identifiers.isEmpty else { return }
Task { try? await manager.loadContacts() }
}
}
}Under .limited, the app can still use Contacts APIs, but only for contacts the user has granted or the app created. ContactAccessButton works well beside a search field; contactAccessPicker(isPresented:completionHandler:) presents a management sheet and returns identifiers for newly granted contacts only.
Multi-Select Contact Picker
SwiftUI Wrapper for Multi-Selection
import SwiftUI
import ContactsUI
struct MultiContactPicker: UIViewControllerRepresentable {
@Binding var selectedContacts: [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: MultiContactPicker
init(_ parent: MultiContactPicker) {
self.parent = parent
}
func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
parent.selectedContacts = contacts
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {}
}
}Email-Only Picker
Configure the picker to only return email addresses.
struct EmailPicker: UIViewControllerRepresentable {
@Binding var selectedEmail: String?
func makeUIViewController(context: Context) -> CNContactPickerViewController {
let picker = CNContactPickerViewController()
picker.delegate = context.coordinator
// Only show contacts with emails
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
// Show contact detail so user can pick a specific email
picker.predicateForSelectionOfProperty = NSPredicate(
format: "key == 'emailAddresses'"
)
picker.displayedPropertyKeys = [CNContactEmailAddressesKey]
return picker
}
func updateUIViewController(_ uiViewController: CNContactPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, CNContactPickerDelegate {
let parent: EmailPicker
init(_ parent: EmailPicker) {
self.parent = parent
}
func contactPicker(
_ picker: CNContactPickerViewController,
didSelect contactProperty: CNContactProperty
) {
parent.selectedEmail = contactProperty.value as? String
}
}
}Search and Filtering
Predicate-Based Search
// By name
let namePredicate = CNContact.predicateForContacts(matchingName: "John")
// By email address
let emailPredicate = CNContact.predicateForContacts(matchingEmailAddress: "john@example.com")
// By phone number
let phonePredicate = CNContact.predicateForContacts(
matching: CNPhoneNumber(stringValue: "+1234567890")
)
// By identifiers (batch fetch)
let idsPredicate = CNContact.predicateForContacts(withIdentifiers: ["id1", "id2", "id3"])
// By group
let groupPredicate = CNContact.predicateForContactsInGroup(withIdentifier: groupId)
// By container
let containerPredicate = CNContact.predicateForContactsInContainer(
withIdentifier: containerId
)Custom Filtering After Fetch
For complex filtering not supported by predicates, enumerate and filter in memory.
func fetchContactsWithBirthday(in month: Int) throws -> [CNContact] {
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor
]
let request = CNContactFetchRequest(keysToFetch: keys)
var contacts: [CNContact] = []
try store.enumerateContacts(with: request) { contact, _ in
if let birthday = contact.birthday, birthday.month == month {
contacts.append(contact)
}
}
return contacts
}vCard Import and Export
Exporting Contacts to vCard
func exportToVCard(contacts: [CNContact]) throws -> Data {
return try CNContactVCardSerialization.data(with: contacts)
}
// Save to file
func saveVCard(contacts: [CNContact], to url: URL) throws {
let data = try CNContactVCardSerialization.data(with: contacts)
try data.write(to: url)
}Importing Contacts from vCard
func importFromVCard(data: Data) throws -> [CNContact] {
return try CNContactVCardSerialization.contacts(with: data)
}
// Save imported contacts to the store
func importAndSave(data: Data) throws {
let contacts = try CNContactVCardSerialization.contacts(with: data)
let saveRequest = CNSaveRequest()
for contact in contacts {
guard let mutable = contact.mutableCopy() as? CNMutableContact else { continue }
saveRequest.add(mutable, toContainerWithIdentifier: nil)
}
try store.execute(saveRequest)
}Contact Groups
Fetching Groups
func fetchGroups() throws -> [CNGroup] {
return try store.groups(matching: nil) // nil returns all groups
}
func fetchContactsInGroup(_ group: CNGroup) throws -> [CNContact] {
let predicate = CNContact.predicateForContactsInGroup(withIdentifier: group.identifier)
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor
]
return try store.unifiedContacts(matching: predicate, keysToFetch: keys)
}Creating and Managing Groups
func createGroup(name: String) throws {
let group = CNMutableGroup()
group.name = name
let saveRequest = CNSaveRequest()
saveRequest.add(group, toContainerWithIdentifier: nil)
try store.execute(saveRequest)
}
func addContactToGroup(contact: CNContact, group: CNGroup) throws {
let saveRequest = CNSaveRequest()
saveRequest.addMember(contact, to: group)
try store.execute(saveRequest)
}
func removeContactFromGroup(contact: CNContact, group: CNGroup) throws {
let saveRequest = CNSaveRequest()
saveRequest.removeMember(contact, from: group)
try store.execute(saveRequest)
}Change Notifications and Swift Boundaries
For Swift-first apps, use CNContactStoreDidChange to invalidate caches and refetch the contacts your authorization status allows. Do not write Swift code that calls store.enumerateChanges(matching:); that method does not exist. Apple's change-history fetch entry point is the Objective-C enumeratorForChangeHistoryFetchRequest:error: selector, while the Swift enumerator(for:) overlay is marked unavailable. If a product truly needs incremental history tokens, isolate that bridge in Objective-C and expose a small Swift wrapper; otherwise, refetch on change notifications.
Watching for Real-Time Changes
func observeChanges(handler: @escaping () -> Void) -> NSObjectProtocol {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
handler()
}
}Related skills
How it compares
Use contacts-framework for native CNContactStore and system picker integration rather than third-party address-book SDKs when the app only needs standard iOS Contacts access.
FAQ
Does the contact picker require full Contacts permission?
No. CNContactPickerViewController returns only user-selected contacts without full read access.
What causes CNContactPropertyNotFetchedException?
Accessing properties not included in keysToFetch when loading a contact.
When is the contacts.notes entitlement required?
Only when reading or writing CNContactNoteKey; it needs Apple approval before distribution.
Is Contacts Framework safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.