contacts-framework by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill contacts-framework使用 CNContactStore、CNSaveRequest 和 CNContactPickerViewController 从用户的通讯录数据库中获取、创建、更新和选择联系人。目标平台为 Swift 6.2 / iOS 26+。
NSContactsUsageDescription,说明应用为何需要访问通讯录com.apple.developer.contacts.notes 授权import Contacts // CNContactStore, CNSaveRequest, CNContact
import ContactsUI // CNContactPickerViewController
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在获取或保存联系人之前请求访问权限。选择器 (CNContactPickerViewController) 不需要授权——系统仅授予用户所选联系人的访问权限。
let store = CNContactStore()
func requestAccess() async throws -> Bool {
return try await store.requestAccess(for: .contacts)
}
// 检查当前状态而不提示用户
func checkStatus() -> CNAuthorizationStatus {
CNContactStore.authorizationStatus(for: .contacts)
}
| 状态 | 含义 |
|---|---|
.notDetermined | 尚未提示用户 |
.authorized | 已授予完整的读写权限 |
.denied | 用户拒绝访问;引导至设置 |
.restricted | 家长控制或 MDM 限制了访问 |
.limited | iOS 18+:用户仅授予了选定联系人的访问权限 |
使用 unifiedContacts(matching:keysToFetch:) 进行基于谓词的查询。使用 enumerateContacts(with:usingBlock:) 对所有联系人进行批量枚举。
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)
}
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)
}
在非主线程上执行 I/O 密集型的枚举操作。
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
}
仅获取你需要的属性。访问未获取的属性会抛出 CNContactPropertyNotFetchedException。
| 键 | 属性 |
|---|---|
CNContactGivenNameKey | 名字 |
CNContactFamilyNameKey | 姓氏 |
CNContactPhoneNumbersKey | 电话号码数组 |
CNContactEmailAddressesKey | 电子邮件地址数组 |
CNContactPostalAddressesKey | 邮寄地址数组 |
CNContactImageDataKey | 全分辨率联系人照片 |
CNContactThumbnailImageDataKey | 缩略图联系人照片 |
CNContactBirthdayKey | 生日日期组件 |
CNContactOrganizationNameKey | 公司名称 |
使用 CNContactFormatter.descriptorForRequiredKeys(for:) 来获取格式化联系人姓名所需的所有键。
let nameKeys = CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
let keys: [CNKeyDescriptor] = [nameKeys, CNContactPhoneNumbersKey as CNKeyDescriptor]
使用 CNMutableContact 构建新联系人,使用 CNSaveRequest 来持久化更改。
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 = 默认容器
try store.execute(saveRequest)
}
你必须先获取你打算修改的属性对应的联系人,创建一个可变副本,更改属性,然后保存。
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)
}
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)
}
CNContactPickerViewController 允许用户选择联系人,而无需授予完整的通讯录访问权限。应用仅接收所选的联系人数据。
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
}
}
}
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("选择联系人") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker(selectedContact: $selectedContact)
}
}
}
使用谓词来控制显示哪些联系人以及用户可以选择什么。
let picker = CNContactPickerViewController()
// 仅显示有电子邮件地址的联系人
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
// 选择联系人直接返回(不显示详情卡片)
picker.predicateForSelectionOfContact = NSPredicate(value: true)
监听外部通讯录数据库的变更以刷新缓存数据。
func observeContactChanges() {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
// 重新获取联系人——缓存的 CNContact 对象已过时
refreshContacts()
}
}
过度获取会浪费内存并减慢查询速度,特别是对于包含大照片的联系人。
// 错误:获取所有内容,包括全分辨率照片
let keys: [CNKeyDescriptor] = [CNContactCompleteNameKey as CNKeyDescriptor,
CNContactImageDataKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor]
// 正确:仅获取你需要显示的内容
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor
]
访问不在 keysToFetch 中的属性会在运行时抛出 CNContactPropertyNotFetchedException。
// 错误:仅获取了姓名键,现在却访问电话
let keys: [CNKeyDescriptor] = [CNContactGivenNameKey as CNKeyDescriptor]
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
let phone = contact.phoneNumbers.first // 崩溃
// 正确:包含你需要的键
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
CNContact 是不可变的。你必须调用 mutableCopy() 来获取 CNMutableContact。
// 错误:CNContact 没有 setter
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
contact.givenName = "新名字" // 编译错误
// 正确:创建可变副本
guard let mutable = contact.mutableCopy() as? CNMutableContact else { return }
mutable.givenName = "新名字"
如果不调用 requestAccess(for:),获取方法将返回空结果或抛出异常。
// 错误:直接跳转到获取
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
// 正确:先检查或请求访问权限
let granted = try await store.requestAccess(for: .contacts)
guard granted else { return }
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
enumerateContacts 执行 I/O 操作。在主线程上运行会阻塞 UI。
// 错误:主线程枚举
func loadContacts() {
try store.enumerateContacts(with: request) { contact, _ in ... }
}
// 正确:在后台线程上运行
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
}
NSContactsUsageDescription 添加到 Info.plistrequestAccess(for: .contacts)CNKeyDescriptor 键CNContactFormatter.descriptorForRequiredKeys(for:)mutableCopy() 创建了可变副本CNSaveRequestenumerateContacts) 在非主线程上运行CNContactStoreDidChange 以刷新缓存的联系人CNContactPickerViewControllerCNContactStore 实例references/contacts-patterns.md每周安装量
327
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex324
opencode321
github-copilot321
amp321
cline321
kimi-cli321
Fetch, create, update, and pick contacts from the user's Contacts database using CNContactStore, CNSaveRequest, and CNContactPickerViewController. Targets Swift 6.2 / iOS 26+.
NSContactsUsageDescription to Info.plist explaining why the app accesses contactscom.apple.developer.contacts.notes entitlementimport Contacts // CNContactStore, CNSaveRequest, CNContact
import ContactsUI // CNContactPickerViewController
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)
}
| 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 |
Use unifiedContacts(matching:keysToFetch:) for predicate-based queries. Use enumerateContacts(with:usingBlock:) for batch enumeration of all contacts.
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)
}
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)
}
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
}
Only fetch the properties you need. Accessing an unfetched property throws CNContactPropertyNotFetchedException.
| Key | Property |
|---|---|
CNContactGivenNameKey | First name |
CNContactFamilyNameKey | Last name |
CNContactPhoneNumbersKey | Phone numbers array |
CNContactEmailAddressesKey | Email addresses array |
CNContactPostalAddressesKey | Mailing addresses array |
CNContactImageDataKey |
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]
Use CNMutableContact to build new contacts and CNSaveRequest to persist changes.
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)
}
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)
}
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)
}
CNContactPickerViewController lets users pick contacts without granting full Contacts access. The app receives only the selected contact data.
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
}
}
}
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)
}
}
}
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)
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()
}
}
Over-fetching wastes memory and slows queries, especially for contacts with large photos.
// WRONG: Fetches everything including full-resolution photos
let keys: [CNKeyDescriptor] = [CNContactCompleteNameKey as CNKeyDescriptor,
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
]
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
]
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"
Without calling requestAccess(for:), fetch methods return empty results or throw.
// 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)
enumerateContacts performs I/O. Running it on the main thread blocks the UI.
// 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
}
NSContactsUsageDescription added to Info.plistrequestAccess(for: .contacts) called before fetch or save operationsCNKeyDescriptor keys included in fetch requestsCNContactFormatter.descriptorForRequiredKeys(for:) used when formatting namesmutableCopy() before modifying contactsCNSaveRequest used for all create/update/delete operationsenumerateContacts) run off the main threadCNContactStoreDidChange observed to refresh cached contactsreferences/contacts-patterns.mdWeekly Installs
327
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex324
opencode321
github-copilot321
amp321
cline321
kimi-cli321
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
105,000 周安装
| Full-resolution contact photo |
CNContactThumbnailImageDataKey | Thumbnail contact photo |
CNContactBirthdayKey | Birthday date components |
CNContactOrganizationNameKey | Company name |
CNContactPickerViewControllerCNContactStore instance reused across the app