contacts-framework

contacts-framework

熱門

使用 Contacts 與 ContactsUI 框架讀取、建立、更新及選取聯絡人。適用於擷取聯絡人資料、儲存新聯絡人、在 SwiftUI 中包裝 CNContactPickerViewController、處理聯絡人權限,或操作 CNContactStore 的擷取與儲存請求。

936星標
47分支
更新於 2026/7/15
SKILL.md
readonlyread-only
name
contacts-framework
description

使用 Contacts 與 ContactsUI 框架讀取、建立、更新及選取聯絡人。適用於擷取聯絡人資料、儲存新聯絡人、在 SwiftUI 中包裝 CNContactPickerViewController、處理聯絡人權限,或操作 CNContactStore 的擷取與儲存請求。

Contacts 框架

在 Swift 6.3 / iOS 26+ 的 App 中,使用 CNContactStoreCNSaveRequestCNContactPickerViewController 來擷取、建立、更新或選取聯絡人。

目錄

設定

專案設定

  1. 在 Info.plist 中加入 NSContactsUsageDescription,說明 App 為何需要存取聯絡人。若未加入此金鑰,使用聯絡人資料 API 會導致 App 當機。
  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 皆為可用的聯絡人 API 狀態。在 .limited 狀態下,擷取、編輯與刪除操作僅適用於使用者授予或 App 建立的聯絡人。使用 ContactAccessButtoncontactAccessPicker(isPresented:completionHandler:) 讓使用者將更多聯絡人加入 App 的有限存取集合。

擷取聯絡人

使用 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) 若未拋出錯誤,即表示儲存成功。僅在該回傳後才更新 App 端的快取或成功 UI。若拋出錯誤,應顯示或傳遞錯誤,保留未儲存的意圖供使用者使用,並修正已知原因(如授權、唯讀容器或無效輸入),然後建立新的請求。序列化重疊的儲存操作,不要在 execute(_:) 使用請求時存取它,並在權限允許時重新擷取可能過時的聯絡人後再進行修正重試。不要盲目重複相同的破壞性請求,也不要要求目前存取層級可能不允許的通用回讀。請參閱擴展聯絡人模式以了解多選、vCard 與最佳化搜尋的工作流程。

聯絡人選取器

CNContactPickerViewController 讓使用者選取聯絡人,無需授予完整的聯絡人存取權。App 只會收到選取的聯絡人資料。

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(_:) 成功後才推進 App 狀態,並在修正請求前顯示失敗
  • [ ] 大量擷取(enumerateContacts)在主執行緒之外執行
  • [ ] 監聽 CNContactStoreDidChange 以重新整理快取聯絡人
  • [ ] 當不需要完整聯絡人存取權時,使用 CNContactPickerViewController
  • [ ] 在顯示選取器視圖控制器之前設定選取器條件
  • [ ] 在整個 App 中重複使用單一 CNContactStore 實例

參考資料