Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
Overflow reference for the contacts-framework skill. Contains advanced patterns that exceed the main skill file's scope.
@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)"
}
}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.
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) {}
}
}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
}
}
}// 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
)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
}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)
}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)
}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)
}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)
}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.
func observeChanges(handler: @escaping () -> Void) -> NSObjectProtocol {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
handler()
}
}.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references