contacts-framework

contacts-framework

热门

使用 Contacts 和 ContactsUI 框架读取、创建、更新和选取联系人。适用于获取联系人数据、保存新联系人、在 SwiftUI 中封装 CNContactPickerViewController、处理联系人权限,或使用 CNContactStore 的获取和保存请求。

936Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
contacts-framework
description

使用 Contacts 和 ContactsUI 框架读取、创建、更新和选取联系人。适用于获取联系人数据、保存新联系人、在 SwiftUI 中封装 CNContactPickerViewController、处理联系人权限,或使用 CNContactStore 的获取和保存请求。

Contacts 框架

在 Swift 6.3 / iOS 26+ 应用中使用 CNContactStoreCNSaveRequestCNContactPickerViewController 来获取、创建、更新或选取联系人。

目录

设置

项目配置

  1. 在 Info.plist 中添加 NSContactsUsageDescription,说明应用访问联系人的原因。如果没有此键,应用在使用联系人数据 API 时会崩溃。
  2. 普通联系人访问不需要额外的能力或授权。
  3. 仅在读取或写入 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 状态下,获取、编辑和删除操作仅适用于用户授予或应用创建的联系人。使用 ContactAccessButtoncontactAccessPicker(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 视为可用访问,并注意所选联系人的限制
  • [ ] 当用户需要扩展有限访问时,提供了 ContactAccessButtoncontactAccessPicker
  • [ ] 优雅处理授权拒绝(引导用户至设置)
  • [ ] 在获取请求中仅包含所需的 CNKeyDescriptor
  • [ ] 在格式化姓名时使用了 CNContactFormatter.descriptorForRequiredKeys(for:)
  • [ ] 在修改联系人之前通过 mutableCopy() 创建了可变副本
  • [ ] 每个创建/更新/删除操作都使用了 CNSaveRequest;仅在 execute(_:) 成功后推进应用状态,并在构建纠正请求之前显示失败信息
  • [ ] 繁重的获取操作(enumerateContacts)在主线程之外运行
  • [ ] 监听了 CNContactStoreDidChange 以刷新缓存的联系人
  • [ ] 当不需要完全联系人访问时,使用了 CNContactPickerViewController
  • [ ] 在选择器视图控制器显示之前设置了选择器谓词
  • [ ] 在整个应用中复用了单个 CNContactStore 实例

参考