关于 contacts-framework
面向 Swift 6.3 / iOS 26+ 开发者的 Contacts 框架技能,涵盖联系人读取、保存与选择。重点说明授权状态、按需获取属性及保存错误处理。
主要功能
- 01读取、创建、更新和删除联系人
- 02按条件或标识符查询
- 03通过系统选择器选择联系人
- 04监听联系人数据库变化
- 05处理授权与有限访问
适用场景
- 01在应用中实现联系人列表或搜索
- 02添加或修改联系人资料
- 03让用户选择联系人而不申请完整访问权限
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill contacts-framework安装后在 Claude Code 中输入/contacts-framework确认已加载。
附带文件evals/evals.jsonreferences/contacts-patterns.md
详细介绍
联系人框架
在 Swift 6.3 / iOS 26+ 应用中,使用 CNContactStore、CNSaveRequest 和 CNContactPickerViewController 获取、创建、更新或选择联系人。
目录
设置
项目配置
- 在 Info.plist 中添加
NSContactsUsageDescription,说明应用访问联系人的原因。如果应用使用联系人数据 API 却未包含此键,会导致崩溃。 - 普通的联系人访问不需要额外的功能或授权。
- 仅在读取或写入
CNContactNoteKey/CNContact.note时添加com.apple.developer.contacts.notes;此授权需要获得 Apple 批准后才能公开发布。
导入
@preconcurrency import Contacts // CNContactStore, CNSaveRequest, CNContact
import ContactsUI // CNContactPickerViewController
授权
获取或保存联系人前,请先请求访问权限。选择器(CNContactPickerViewController)不需要授权——系统仅会授予访问用户所选联系人的权限。
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)
}
授权状态
| 状态 | 含义 |
|---|---|
.notDetermined | 尚未向用户请求授权 |
.authorized | 已授予完整的读写权限 |
.denied | 用户拒绝了访问;请引导用户前往“设置” |
.restricted | 家长控制或 MDM 限制了访问 |
.limited | iOS 18+:用户仅授予访问所选联系人的权限 |
.authorized 和 .limited 都应视为可用的 Contacts API 状态。在 .limited 状态下,获取、编辑和删除操作仅适用于用户授权的联系人或应用创建的联系人。使用 ContactAccessButton 或 contactAccessPicker(isPresented:completionHandler:),让用户将联系人添加到应用的有限访问范围。
获取联系人
使用 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 = default container
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)
}
保存结果与恢复
try store.execute(saveRequest) 返回且未抛出错误,即表示保存成功。只有在该调用返回后,才更新应用端缓存或成功状态界面。如果调用抛出错误,请显示或传递该错误,保留用户尚未保存的操作意图,并先修正已知原因(例如授权问题、容器为只读或输入无效),再构建新的请求。串行处理重叠的保存操作;execute(_:) 正在使用请求时,不要访问该请求;如果权限允许,在修正后重试前重新获取可能已过期的联系人。不要盲目重复相同的破坏性请求,也不要强制要求进行当前访问级别可能不允许的通用回读。对于多选、vCard 和优化搜索工作流,请阅读
Extended Contacts Patterns。
联系人选择器
CNContactPickerViewController 允许用户选择联系人,而无需授予完整的“通讯录”访问权限。应用只会收到用户选中的联系人数据。
SwiftUI 封装
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)
}
}
}
筛选选择器内容
使用谓词控制显示哪些联系人,以及用户可以选择哪些联系人。
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)
监听更改
监听外部通讯录数据库的更改,以刷新缓存数据。
func observeContactChanges() {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
// Refetch contacts -- cached CNContact objects are stale
refreshContacts()
}
}
常见错误
不要:只需姓名时却获取所有键
过度获取会浪费内存并降低查询速度,尤其是对于包含大幅照片的联系人。
// 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
]
不要:访问未获取的属性
访问未包含在 keysToFetch 中的属性会在运行时抛出
CNContactPropertyNotFetchedException。
// 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
CNContact 是不可变的。必须调用 mutableCopy() 来获取 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"
不要:跳过授权并假定已有访问权限
不要让获取或保存调用成为用户首次看到授权请求的时机。如果状态为 .notDetermined,请请求访问权限;如果访问被拒绝,联系人操作会因授权错误而失败。
// 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 会执行 I/O。在主线程上运行会阻塞 UI。
如果严格并发检查提示 CNContact 跨越任务或 Actor 边界,可在该文件中使用 @preconcurrency import Contacts,或在返回前将联系人映射为 Sendable 视图模型。
// 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添加到 Info.plist - 在获取或保存操作前调用了
requestAccess(for: .contacts) - 将
.limited视为可用访问权限,同时注意所选联系人的相关限制 - 用户需要扩大有限访问范围时,提供
ContactAccessButton或contactAccessPicker - 妥善处理授权被拒(引导用户前往“设置”)
- 获取请求中只包含所需的
CNKeyDescriptor键 - 格式化姓名时使用了
CNContactFormatter.descriptorForRequiredKeys(for:) - 修改联系人前通过
mutableCopy()创建了可变副本 - 每项创建、更新和删除操作都使用
CNSaveRequest;只有在execute(_:)成功后才推进应用状态,并且在构建修正后的请求前显示失败信息 - 在主线程之外运行耗时的获取操作(
enumerateContacts) - 监听了
CNContactStoreDidChange,以刷新缓存的联系人 - 不需要完整“通讯录”访问权限时,使用
CNContactPickerViewController - 在展示选择器视图控制器前设置了选择器谓词
- 整个应用复用同一个
CNContactStore实例
参考资料
- 扩展模式(多选选择器、vCard 导出、搜索优化):references/contacts-patterns.md
- Contacts 框架
- CNContactStore
- CNContactFetchRequest
- CNSaveRequest
- CNMutableContact
- CNContactPickerViewController
- CNContactPickerDelegate
- 访问联系人存储
- NSContactsUsageDescription
- ContactAccessButton
- contactAccessPicker(isPresented:completionHandler:)
- 联系人键