contacts-framework について
Swift 6.3 / iOS 26以降のアプリ開発者向けに、Contactsフレームワークの使い方を解説するスキルです。認証状態、必要なプロパティだけの取得、保存エラーへの対応を扱います。
主な機能
- 01連絡先の取得、作成、更新、削除
- 02条件または識別子による検索
- 03システムピッカーでの連絡先選択
- 04連絡先データベースの変更監視
- 05認証と限定アクセスへの対応
活用シーン
- 01アプリ内の連絡先一覧や検索の実装
- 02連絡先情報の追加や編集
- 03完全なアクセス権なしでの連絡先選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill contacts-frameworkインストール後、Claude Code で/contacts-frameworkを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/contacts-patterns.md
詳細
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)
}
// Check current status without prompting
func checkStatus() -> CNAuthorizationStatus {
CNContactStore.authorizationStatus(for: .contacts)
}
認証状態
| 状態 | 意味 |
|---|---|
.notDetermined | ユーザーへの確認はまだ行われていません |
.authorized | 読み書きの完全なアクセスが許可されています |
.denied | ユーザーがアクセスを拒否しました。設定画面に誘導してください |
.restricted | ペアレンタルコントロールまたは MDM によってアクセスが制限されています |
.limited | iOS 18 以降:ユーザーが選択した連絡先のみへのアクセスが許可されています |
.authorized と .limited のどちらも、連絡先 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 = default container
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、最適化された検索のワークフローについては、
Extended Contacts Patterns を参照してください。
連絡先ピッカー
CNContactPickerViewController を使うと、Contacts への完全なアクセスを許可せずに、ユーザーが連絡先を選択できます。アプリが受け取るのは、選択された連絡先データのみです。
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("Select Contact") {
showPicker = true
}
}
.sheet(isPresented: $showPicker) {
ContactPicker(selectedContact: $selectedContact)
}
}
}
ピッカーのフィルタリング
述語を使って、表示する連絡先とユーザーが選択できる項目を制御します。
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)
変更の監視
外部で連絡先データベースが変更されたときに通知を受け取り、キャッシュされたデータを更新します。
func observeContactChanges() {
NotificationCenter.default.addObserver(
forName: .CNContactStoreDidChange,
object: nil,
queue: .main
) { _ in
// Refetch contacts -- cached CNContact objects are stale
refreshContacts()
}
}
よくある間違い
やってはいけないこと:名前だけが必要なときにすべてのキーを取得する
過剰な取得はメモリを浪費し、クエリを遅くします。特に、大きな写真を含む連絡先では顕著です。
// WRONG: Fetches far more than the UI displays, including full-resolution photos
let keys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
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
]
やってはいけないこと:取得していないプロパティにアクセスする
keysToFetch に含まれていないプロパティにアクセスすると、実行時に
CNContactPropertyNotFetchedException がスローされます。
// 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
]
やってはいけないこと:CNContact を直接変更する
CNContact は不変です。mutableCopy() を呼び出して CNMutableContact を取得する必要があります。
// 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"
やってはいけないこと:認証を省略してアクセスできると想定する
取得または保存の呼び出しが、ユーザーが初めて認証要求を目にするタイミングにならないようにしてください。ステータスが .notDetermined の場合はアクセスを要求します。アクセスが拒否されている場合、連絡先操作は認証エラーで失敗します。
// 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)
やってはいけないこと:負荷の高い取得をメインスレッドで実行する
enumerateContacts は I/O を実行します。メインスレッドで実行すると UI がブロックされます。厳密な並行性チェックで、タスクまたはアクターの境界を越えて CNContact を渡すことについて警告が表示される場合は、そのファイルで @preconcurrency import Contacts を使用するか、連絡先を Sendable なビューモデルに変換してから返してください。
// 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
}
レビューチェックリスト
- Info.plist に
NSContactsUsageDescriptionを追加した - 取得または保存の操作前に
requestAccess(for: .contacts)を呼び出した -
.limitedを、選択された連絡先に関する注意点を踏まえて利用可能なアクセスとして扱っている - ユーザーが制限付きアクセスの範囲を広げる必要がある場合に、
ContactAccessButtonまたはcontactAccessPickerを提示している - 認証の拒否を適切に処理している(ユーザーを「設定」に案内する)
- 取得リクエストには、必要な
CNKeyDescriptorキーのみを含めている - 名前のフォーマットに
CNContactFormatter.descriptorForRequiredKeys(for:)を使用している - 連絡先を変更する前に
mutableCopy()でミュータブルなコピーを作成している - 作成/更新/削除のすべてで
CNSaveRequestを使用している。アプリの状態を進めるのはexecute(_:)の成功後のみとし、修正したリクエストを作成する前に失敗を通知している - 負荷の高い取得(
enumerateContacts)をメインスレッド以外で実行している - キャッシュされた連絡先を更新するために
CNContactStoreDidChangeを監視している - Contacts への完全なアクセスが不要な場合に
CNContactPickerViewControllerを使用している - ピッカービューコントローラーを表示する前に、ピッカーの述語を設定している
- アプリ全体で単一の
CNContactStoreインスタンスを再利用している
参考資料
- 拡張パターン(複数選択ピッカー、vCard のエクスポート、検索の最適化):references/contacts-patterns.md
- 連絡先フレームワーク
- CNContactStore
- CNContactFetchRequest
- CNSaveRequest
- CNMutableContact
- CNContactPickerViewController
- CNContactPickerDelegate
- 連絡先ストアへのアクセス
- NSContactsUsageDescription
- ContactAccessButton
- contactAccessPicker(isPresented:completionHandler:)
- 連絡先キー