Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

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)
At a glance

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
From the docs

What contacts-framework says it does

The app crashes if it uses contact data APIs without this key.
SKILL.md
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill contacts-framework

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.6k
repo stars944
Security audit3 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/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

SKILL.mdMarkdownGitHub ↗

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

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                // 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

StatusMeaning
.notDeterminedUser has not been prompted yet
.authorizedFull read/write access granted
.deniedUser denied access; direct to Settings
.restrictedParental controls or MDM restrict access
.limitediOS 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

KeyProperty
CNContactGivenNameKeyFirst name
CNContactFamilyNameKeyLast name
CNContactPhoneNumbersKeyPhone numbers array
CNContactEmailAddressesKeyEmail addresses array
CNContactPostalAddressesKeyMailing addresses array
CNContactImageDataKeyFull-resolution contact photo
CNContactThumbnailImageDataKeyThumbnail contact photo
CNContactBirthdayKeyBirthday date components
CNContactOrganizationNameKeyCompany 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

  • [ ] NSContactsUsageDescription added to Info.plist
  • [ ] requestAccess(for: .contacts) called before fetch or save operations
  • [ ] .limited treated as usable access with selected-contact caveats
  • [ ] ContactAccessButton or contactAccessPicker offered when users need to expand limited access
  • [ ] Authorization denial handled gracefully (guide user to Settings)
  • [ ] Only needed CNKeyDescriptor keys included in fetch requests
  • [ ] CNContactFormatter.descriptorForRequiredKeys(for:) used when formatting names
  • [ ] Mutable copy created via mutableCopy() before modifying contacts
  • [ ] CNSaveRequest used for all create/update/delete operations
  • [ ] Heavy fetches (enumerateContacts) run off the main thread
  • [ ] CNContactStoreDidChange observed to refresh cached contacts
  • [ ] CNContactPickerViewController used when full Contacts access is unnecessary
  • [ ] Picker predicates set before presenting the picker view controller
  • [ ] Single CNContactStore instance reused across the app

References

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.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.