使用 Contacts 與 ContactsUI 框架讀取、建立、更新及選取聯絡人。適用於擷取聯絡人資料、儲存新聯絡人、在 SwiftUI 中包裝 CNContactPickerViewController、處理聯絡人權限,或操作 CNContactStore 的擷取與儲存請求。
Contacts 框架
在 Swift 6.3 / iOS 26+ 的 App 中,使用 CNContactStore、CNSaveRequest 與 CNContactPickerViewController 來擷取、建立、更新或選取聯絡人。
目錄
設定
專案設定
- 在 Info.plist 中加入
NSContactsUsageDescription,說明 App 為何需要存取聯絡人。若未加入此金鑰,使用聯絡人資料 API 會導致 App 當機。 - 一般聯絡人存取不需要額外的能力或授權。
- 只有在讀取或寫入
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 建立的聯絡人。使用 ContactAccessButton 或 contactAccessPicker(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視為可用的存取狀態,並注意選取聯絡人的限制 - [ ] 當使用者需要擴充有限存取時,提供
ContactAccessButton或contactAccessPicker - [ ] 優雅處理授權拒絕(引導使用者至設定)
- [ ] 在擷取請求中只包含需要的
CNKeyDescriptor金鑰 - [ ] 格式化姓名時使用
CNContactFormatter.descriptorForRequiredKeys(for:) - [ ] 修改聯絡人前,透過
mutableCopy()建立可變副本 - [ ] 每個建立/更新/刪除操作都使用
CNSaveRequest;僅在execute(_:)成功後才推進 App 狀態,並在修正請求前顯示失敗 - [ ] 大量擷取(
enumerateContacts)在主執行緒之外執行 - [ ] 監聽
CNContactStoreDidChange以重新整理快取聯絡人 - [ ] 當不需要完整聯絡人存取權時,使用
CNContactPickerViewController - [ ] 在顯示選取器視圖控制器之前設定選取器條件
- [ ] 在整個 App 中重複使用單一
CNContactStore實例
參考資料
- 擴展模式(多選選取器、vCard 匯出、搜尋最佳化):references/contacts-patterns.md
- Contacts 框架
- CNContactStore
- CNContactFetchRequest
- CNSaveRequest
- CNMutableContact
- CNContactPickerViewController
- CNContactPickerDelegate
- 存取聯絡人儲存庫
- NSContactsUsageDescription
- ContactAccessButton
- contactAccessPicker(isPresented:completionHandler:)
- 聯絡人金鑰






