CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

contacts-patterns.mdskills/contacts-framework/references/

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 History

Contacts SwiftUI Integration

Contact Manager with @Observable

import Contacts
import SwiftUI

@Observable
@MainActor
final class ContactManager {
    let store = CNContactStore()

    var contacts: [CNContact] = []
    var isAuthorized = false
    var authorizationStatus: CNAuthorizationStatus = .notDetermined

    func checkAuthorization() {
        authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
        isAuthorized = authorizationStatus == .authorized
    }

    func requestAccess() async throws {
        let granted = try await store.requestAccess(for: .contacts)
        isAuthorized = granted
        authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
    }

    func loadContacts() async throws {
        guard isAuthorized 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.isAuthorized {
                    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
                }
            }
            .navigationTitle("Contacts")
            .task {
                manager.checkAuthorization()
                if manager.isAuthorized {
                    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)
        }
    }
}

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 History

Use CNChangeHistoryFetchRequest to fetch incremental changes since a saved token. This is efficient for syncing contact data.

func fetchChanges(since token: Data?) throws {
    let request = CNChangeHistoryFetchRequest()
    request.startingToken = token
    request.additionalContactKeyDescriptors = [
        CNContactGivenNameKey as CNKeyDescriptor,
        CNContactFamilyNameKey as CNKeyDescriptor
    ]

    let result = try store.enumerateChanges(matching: request)

    for event in result {
        switch event {
        case let addEvent as CNChangeHistoryAddContactEvent:
            let contact = addEvent.contact
            print("Added: \(contact.givenName) \(contact.familyName)")

        case let updateEvent as CNChangeHistoryUpdateContactEvent:
            let contact = updateEvent.contact
            print("Updated: \(contact.givenName) \(contact.familyName)")

        case let deleteEvent as CNChangeHistoryDeleteContactEvent:
            let identifier = deleteEvent.contactIdentifier
            print("Deleted: \(identifier)")

        default:
            break
        }
    }

    // Save the new token for next sync
    let newToken = store.currentHistoryToken
}

Watching for Real-Time Changes

func observeChanges(handler: @escaping () -> Void) -> NSObjectProtocol {
    NotificationCenter.default.addObserver(
        forName: .CNContactStoreDidChange,
        object: nil,
        queue: .main
    ) { _ in
        handler()
    }
}

skills

contacts-framework

CHANGELOG.md

README.md

tile.json