使用 Contacts 和 ContactsUI 框架读取、创建、更新和选取联系人。适用于获取联系人数据、保存新联系人、在 SwiftUI 中封装 CNContactPickerViewController、处理联系人权限,或使用 CNContactStore 的获取和保存请求。
Contacts 框架
在 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)
}
// 检查当前状态而不提示
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 = 默认容器
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) 不抛出异常即表示保存成功。仅在此返回后更新应用端缓存或成功 UI。如果抛出异常,则显示或传播错误,保留未保存的意图供用户使用,并在构建新请求之前纠正已知原因(如授权、只读容器或无效输入)。序列化重叠的保存操作,不要在 execute(_:) 使用请求时访问它,并在访问允许的情况下,在纠正重试之前重新获取可能过时的联系人。不要盲目重复相同的破坏性请求,也不要要求当前访问级别可能不允许的通用回读。加载扩展联系人模式以获取多选、vCard 和优化搜索工作流。
联系人选择器
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("选择联系人") {
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()
}
}
常见错误
不要:仅需名称时获取所有键
过度获取会浪费内存并减慢查询速度,尤其是对于包含大照片的联系人。
// 错误:获取的内容远超 UI 显示,包括全分辨率照片
let keys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
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
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 = "新名字"
不要:跳过授权并假设有访问权限
不要让获取或保存调用成为用户看到授权的第一个地方。如果状态是 .notDetermined,请求访问;如果访问被拒绝,联系人操作会失败并返回授权错误。
// 错误:直接跳转到获取
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。当严格并发检查抱怨 CNContact 跨越任务或 actor 边界时,在该文件中使用 @preconcurrency import Contacts,或在返回之前将联系人映射到 Sendable 视图模型。
// 错误:主线程枚举
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
}
审查清单
- [ ] 在 Info.plist 中添加了
NSContactsUsageDescription - [ ] 在获取或保存操作之前调用了
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:)
- 联系人键






