ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

iOS 連絡先一覧の取得

Contacts Frameworkを使用し、標準の連絡先アプリに登録されている連絡先一覧を取得する方法についてまとめてみます。

Contacts Framework公式ページ

サンプルコードは以下に置いています。

https://github.com/kurozu10344/ContactsSample

検証環境

  • iOS 11 シミュレータ
  • Xcode 9.4.1
  • Swift 4.1

連絡先一覧の取得

利用目的の記述(Info.plist)

まず、このFrameworkはユーザのプライバシーなデータにアクセスするため、ユーザに対して連絡先へアクセスする目的を提示する必要があります。この設定をターゲットのInfo.plistに追記します。

f:id:kurozu10344:20180728234759p:plain

KeyはPrivacy - Contacts Usage Description(Raw Keyの場合はNSContactsUsageDescription)を、TypeはStringを選択し、Valueにユーザに提示する利用目的を記述します。

NSContactsUsageDescription

ここで設定した文言が、後ほど説明するアクセス許可要求時の確認アラートに表示されます。

この設定を行わずにContacts FrameworkのAPIを呼び出そうとした場合、以下のようなエラーメッセージを表示してアプリが落ちます。よって設定は必須です。

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSContactsUsageDescription key with a string value explaining to the user how the app uses this data.

アクセス許可の確認・要求

ここからコーディングに入ります。

まず、実際に連絡先を取得する前に、ユーザに連絡先アクセスの許可を得ているかを確認し、まだの場合はアクセス許可を求める必要があります。

        let status = CNContactStore.authorizationStatus(for: .contacts)
        switch status {
        case .notDetermined:
            // 初回アクセス時
            // アクセス許可要求
            CNContactStore().requestAccess(for: .contacts) { (granted, error) in
                if granted {
                    // アクセス許可された
                }
                else {
                    // アクセス拒否された
                }
            }

        case .authorized:
            // アクセス許可済

        case .restricted:
            // ペアレンタルコントロール等の機能制限により利用不可

        case .denied:
            // アクセス拒否済
        }
  1. CNContactStore.authorizationStatus(for:)を呼び出し、許可状態を確認する。
  2. 許可状態がCNAuthorizationStatus.notDeterminedの場合、CNContactStore.requestAccess(for:completionHandler:)を呼び出し、アクセス許可を要求する。
  3. ↑で渡したcompletionHandlerの第一引数(ソース中ではgranted)がtrueの場合、連絡先へのアクセスが可能になる。

CNContactStore.requestAccess(for:completionHandler:)を呼び出した際に以下のようなアラートが表示され、Info.plistに記述した利用目的が表示されます。

f:id:kurozu10344:20180728234802p:plain

取得処理

アクセスが許可された、あるいは許可済の場合、ようやく連絡先の取得を開始します。

    private func loadContacts() {
        DispatchQueue.global().async {
            let store = CNContactStore()
            let keys: [CNKeyDescriptor] = [
                CNContactFamilyNameKey as CNKeyDescriptor,
                CNContactGivenNameKey as CNKeyDescriptor,
                CNContactEmailAddressesKey as CNKeyDescriptor,
                CNContactPhoneNumbersKey as CNKeyDescriptor,
                CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
            let fetchRequest = CNContactFetchRequest(keysToFetch: keys)
            var contacts: [CNContact] = []
            try? store.enumerateContacts(with: fetchRequest) { contact, cursor in
                contacts.append(contact)
            }
            self.contacts = contacts
            DispatchQueue.main.async {
                // Viewに表示、等
            }
        }
    }
  • self.contactsは[CNContact]型のプロパティです。
  • 連絡先取得にはI/O操作が含まれますが、同期的に実行されるため、バックグラウンドでの処理が推奨されています。

    Because the contact store methods are synchronous, it is recommended that you use them on background threads.

  • keysには連絡先内の必要な項目を列挙します。ここに指定していない項目を取得しようとした場合(例えばCNContactEmailAddressKeyを指定せずにcontact.emailAddressesを呼び出した場合)、アプリがクラッシュします。

サンプルソースではCNContactStore.enumerateContacts(with:usingBlock:)を呼び出し、クロージャの引数に渡された連絡先(contact: CNContact)をリストに詰め込んでいます。

あとはこの連絡先から各種情報を取得して処理していけば良いかと思います。

フルネームの取得

フルネームの取得はCNContactを直接使わず、CNContactFormatter.string(from:style:)を使用します。(まぁ日本人ならfamilyNamegivenNameを組み合わせるだけで良いのかもしれませんが。。。)

let fullName = CNContactFormatter.string(from: contact, style: .fullName)

style: .fullNameの箇所に取得したいデータ形式(CNContactFormatterStyle)を指定します。2018年7月現在はフルネームとフルネームのふりがなが指定可能です。

またこの場合、fetchRequestに指定するkeyに以下を含める必要があります。

CNContactFormatter.descriptorForRequiredKeys(for: .fullName)

自分のカードは判別不可

iOSでは、特定の連絡先を自分の情報として登録が出来、連絡先アプリに自分のカード(My Card)として表示することが出来ますが、サードパーティアプリからはどの連絡先が自分のカードであるかは判別出来ないようです。 CNContactStore.unifiedMeContactWithKeys(toFetch:)というメソッドがありますが、こちらはmacOS専用のAPIのため、iOSからは使用出来ません。

https://forums.developer.apple.com/thread/67586