contacts-framework

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Contacts Framework

Contacts 框架

Fetch, create, update, and pick contacts from the user's Contacts database using
CNContactStore
,
CNSaveRequest
, and
CNContactPickerViewController
. Targets Swift 6.2 / iOS 26+.
使用
CNContactStore
CNSaveRequest
CNContactPickerViewController
从用户的联系人数据库中获取、创建、更新和选择联系人。目标环境为Swift 6.2 / iOS 26+。

Contents

目录

Setup

配置

Project Configuration

项目配置

  1. Add
    NSContactsUsageDescription
    to Info.plist explaining why the app accesses contacts
  2. No additional capability or entitlement is required for basic Contacts access
  3. For contact notes access, add the
    com.apple.developer.contacts.notes
    entitlement
  1. 在Info.plist中添加
    NSContactsUsageDescription
    ,说明应用访问联系人的原因
  2. 基础联系人访问无需额外的功能权限或授权
  3. 若需访问联系人备注,需添加
    com.apple.developer.contacts.notes
    权限

Imports

导入框架

swift
import Contacts       // CNContactStore, CNSaveRequest, CNContact
import ContactsUI     // CNContactPickerViewController
swift
import Contacts       // CNContactStore, CNSaveRequest, CNContact
import ContactsUI     // CNContactPickerViewController

Authorization

权限授权

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.
swift
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)
}
在获取或保存联系人前需请求权限。选择器(
CNContactPickerViewController
)无需授权——系统仅会授予对用户所选联系人的访问权限。
swift
let store = CNContactStore()

func requestAccess() async throws -> Bool {
    return try await store.requestAccess(for: .contacts)
}

// 检查当前权限状态,不触发提示
func checkStatus() -> CNAuthorizationStatus {
    CNContactStore.authorizationStatus(for: .contacts)
}

Authorization States

授权状态

StatusMeaning
.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
状态含义
.notDetermined
用户尚未收到权限提示
.authorized
已授予完整的读写权限
.denied
用户拒绝了权限;引导至设置页面
.restricted
家长控制或MDM限制了访问权限
.limited
iOS 18+:用户仅授予对所选联系人的访问权限

Fetching Contacts

获取联系人

Use
unifiedContacts(matching:keysToFetch:)
for predicate-based queries. Use
enumerateContacts(with:usingBlock:)
for batch enumeration of all contacts.
使用
unifiedContacts(matching:keysToFetch:)
进行基于谓词的查询。使用
enumerateContacts(with:usingBlock:)
批量枚举所有联系人。

Fetch by Name

按姓名获取

swift
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)
}
swift
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)
}

Fetch by Identifier

按标识符获取

swift
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)
}
swift
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)
}

Enumerate All Contacts

枚举所有联系人

Perform I/O-heavy enumeration off the main thread.
swift
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
}
在主线程外执行I/O密集型的枚举操作。
swift
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
}

Key Descriptors

键描述符

Only fetch the properties you need. Accessing an unfetched property throws
CNContactPropertyNotFetchedException
.
仅获取你需要的属性。访问未获取的属性会抛出
CNContactPropertyNotFetchedException
异常。

Common Keys

常用键

KeyProperty
CNContactGivenNameKey
First name
CNContactFamilyNameKey
Last name
CNContactPhoneNumbersKey
Phone numbers array
CNContactEmailAddressesKey
Email addresses array
CNContactPostalAddressesKey
Mailing addresses array
CNContactImageDataKey
Full-resolution contact photo
CNContactThumbnailImageDataKey
Thumbnail contact photo
CNContactBirthdayKey
Birthday date components
CNContactOrganizationNameKey
Company name
属性
CNContactGivenNameKey
名字
CNContactFamilyNameKey
姓氏
CNContactPhoneNumbersKey
电话号码数组
CNContactEmailAddressesKey
邮箱地址数组
CNContactPostalAddressesKey
邮寄地址数组
CNContactImageDataKey
全分辨率联系人照片
CNContactThumbnailImageDataKey
联系人照片缩略图
CNContactBirthdayKey
生日日期组件
CNContactOrganizationNameKey
公司名称

Composite Key Descriptors

复合键描述符

Use
CNContactFormatter.descriptorForRequiredKeys(for:)
to fetch all keys needed for formatting a contact's name.
swift
let nameKeys = CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
let keys: [CNKeyDescriptor] = [nameKeys, CNContactPhoneNumbersKey as CNKeyDescriptor]
使用
CNContactFormatter.descriptorForRequiredKeys(for:)
获取格式化联系人姓名所需的所有键。
swift
let nameKeys = CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
let keys: [CNKeyDescriptor] = [nameKeys, CNContactPhoneNumbersKey as CNKeyDescriptor]

Creating and Updating Contacts

创建和更新联系人

Use
CNMutableContact
to build new contacts and
CNSaveRequest
to persist changes.
使用
CNMutableContact
创建新联系人,使用
CNSaveRequest
持久化更改。

Creating a New Contact

创建新联系人

swift
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)
}
swift
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)
}

Updating an Existing Contact

更新现有联系人

You must fetch the contact with the properties you intend to modify, create a mutable copy, change the properties, then save.
swift
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)
}
你必须先获取包含要修改属性的联系人,创建可变副本,修改属性后再保存。
swift
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)
}

Deleting a Contact

删除联系人

swift
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)
}
swift
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)
}

Contact Picker

联系人选择器

CNContactPickerViewController
lets users pick contacts without granting full Contacts access. The app receives only the selected contact data.
CNContactPickerViewController
允许用户选择联系人,无需授予完整的联系人访问权限。应用仅会收到用户所选的联系人数据。

SwiftUI Wrapper

SwiftUI 封装

swift
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
        }
    }
}
swift
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
        }
    }
}

Using the Picker

使用选择器

swift
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)
        }
    }
}
swift
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)
        }
    }
}

Filtering the Picker

过滤选择器

Use predicates to control which contacts appear and what the user can select.
swift
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)
使用谓词控制显示哪些联系人以及用户可以选择的内容。
swift
let picker = CNContactPickerViewController()
// 仅显示有邮箱地址的联系人
picker.predicateForEnablingContact = NSPredicate(format: "emailAddresses.@count > 0")
// 选择联系人后直接返回(不显示详情卡片)
picker.predicateForSelectionOfContact = NSPredicate(value: true)

Observing Changes

监听变更

Listen for external contact database changes to refresh cached data.
swift
func observeContactChanges() {
    NotificationCenter.default.addObserver(
        forName: .CNContactStoreDidChange,
        object: nil,
        queue: .main
    ) { _ in
        // Refetch contacts -- cached CNContact objects are stale
        refreshContacts()
    }
}
监听外部联系人数据库的变更以刷新缓存数据。
swift
func observeContactChanges() {
    NotificationCenter.default.addObserver(
        forName: .CNContactStoreDidChange,
        object: nil,
        queue: .main
    ) { _ in
        // 重新获取联系人——缓存的CNContact对象已过期
        refreshContacts()
    }
}

Common Mistakes

常见错误

DON'T: Fetch all keys when you only need a name

不要:仅需要姓名时获取所有键

Over-fetching wastes memory and slows queries, especially for contacts with large photos.
swift
// 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
]
过度获取会浪费内存并减慢查询速度,尤其是对于包含大尺寸照片的联系人。
swift
// 错误:获取所有内容,包括全分辨率照片
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
]

DON'T: Access unfetched properties

不要:访问未获取的属性

Accessing a property that was not in
keysToFetch
throws
CNContactPropertyNotFetchedException
at runtime.
swift
// 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
]
访问未包含在
keysToFetch
中的属性会在运行时抛出
CNContactPropertyNotFetchedException
异常。
swift
// 错误:仅获取了姓名键,现在访问电话
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
]

DON'T: Mutate a CNContact directly

不要:直接修改CNContact

CNContact
is immutable. You must call
mutableCopy()
to get a
CNMutableContact
.
swift
// 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"
CNContact
是不可变的。你必须调用
mutableCopy()
来获取
CNMutableContact
swift
// 错误:CNContact没有设置器
let contact = try store.unifiedContact(withIdentifier: id, keysToFetch: keys)
contact.givenName = "新名称" // 编译错误

// 正确:创建可变副本
guard let mutable = contact.mutableCopy() as? CNMutableContact else { return }
mutable.givenName = "新名称"

DON'T: Skip authorization and assume access

不要:跳过授权直接访问

Without calling
requestAccess(for:)
, fetch methods return empty results or throw.
swift
// 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)
如果未调用
requestAccess(for:)
,获取方法会返回空结果或抛出异常。
swift
// 错误:直接跳转到获取操作
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)

DON'T: Run heavy fetches on the main thread

不要:在主线程执行大量获取操作

enumerateContacts
performs I/O. Running it on the main thread blocks the UI.
swift
// 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
}
enumerateContacts
会执行I/O操作。在主线程运行会阻塞UI。
swift
// 错误:在主线程枚举
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
}

Review Checklist

检查清单

  • NSContactsUsageDescription
    added to Info.plist
  • requestAccess(for: .contacts)
    called before fetch or save operations
  • Authorization denial handled gracefully (guide user to Settings)
  • Only needed
    CNKeyDescriptor
    keys included in fetch requests
  • CNContactFormatter.descriptorForRequiredKeys(for:)
    used when formatting names
  • Mutable copy created via
    mutableCopy()
    before modifying contacts
  • CNSaveRequest
    used for all create/update/delete operations
  • Heavy fetches (
    enumerateContacts
    ) run off the main thread
  • CNContactStoreDidChange
    observed to refresh cached contacts
  • CNContactPickerViewController
    used when full Contacts access is unnecessary
  • Picker predicates set before presenting the picker view controller
  • Single
    CNContactStore
    instance reused across the app
  • 已在Info.plist中添加
    NSContactsUsageDescription
  • 在执行获取或保存操作前调用了
    requestAccess(for: .contacts)
  • 已优雅处理授权拒绝(引导用户至设置页面)
  • 获取请求中仅包含所需的
    CNKeyDescriptor
  • 格式化姓名时使用了
    CNContactFormatter.descriptorForRequiredKeys(for:)
  • 修改联系人前通过
    mutableCopy()
    创建了可变副本
  • 所有创建/更新/删除操作都使用了
    CNSaveRequest
  • 大量获取操作(
    enumerateContacts
    )在主线程外执行
  • 监听了
    CNContactStoreDidChange
    以刷新缓存的联系人
  • 无需完整联系人访问时使用了
    CNContactPickerViewController
  • 展示选择器视图控制器前设置了选择器谓词
  • 应用中复用了单个
    CNContactStore
    实例

References

参考资料