[CallKit] 思わぬハマリどころも!Call Directory Extensionを使って着信時に発信者名を表示する | Developers.IO

[CallKit] 思わぬハマリどころも!Call Directory Extensionを使って着信時に発信者名を表示する

CallKitとは

CallKitはiOS 10から登場したフレームワークで、通話サービスをシステム上の通話関連アプリと統合することができます。 具体的には例えば、「電話」アプリと同様のUIを提供したり、自分で作ったアプリの通話履歴が「電話」アプリの履歴に表示され、その履歴から自分で作ったアプリを使って電話をかけることができるようになりました。

さらに、App ExtensionにもCall Directory Extensionが追加され、iOS標準の「連絡先」アプリではなく、自分の作るアプリが管理している電話帳データを使った着信拒否設定や発信者名表示が行えるようになりました。 今回はCall Directory Extensionを使った発信者名表示の実装方法をご紹介します。

検証環境

本エントリは以下の環境で検証を行っています。

  • macOS Sierra バージョン 10.12.6
  • Xcode Version 9.2 (9C40b)
  • Swift 4

Call Directory Extensionを作成する

Xcodeで任意のプロジェクトがある前提で話を進めます。

プロジェクトを選択し、「+」ボタンをクリックします。

テンプレートを選ぶ画面が表示されるので、「Call Directory Extension」を選択して「Next」をクリックします。

任意のProduct Nameを入力して「Finish」をクリックします。

作成したCall Directory Extensionターゲットに対するスキームをアクティベートするかどうかを聞かれるので「Activate」をクリックします。

完了すると、Project navigator上に以下が追加されます。

  • 〜Handler.swift
  • Info.plit

swiftファイルの方に発信者名表示のための処理を実装します。

発信者名表示のためのデータベースを提供する

自動生成されたswiftファイルを開くと、すでにテンプレート実装が書かれています。 CXCallDirectoryProviderのサブクラスである〜Handlerクラスが定義されていて、beginRequest(with:)メソッドがオーバーライドされています。 このメソッドの中で発信者名表示のためのデータベースを提供する処理 を記述していきます。

以下は実装例です。

class CallDirectoryHandler: CXCallDirectoryProvider {
    override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        // コンテキストのデリテートを設定することでリクエスト失敗時のエラーが拾えるようになる。
        context.delegate = self

        // コンテキストに発信者を識別するエントリー(電話番号とラベルのセット)を追加する。
        let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 81_8011111111, 81_8011111112 ]
        let labels = [ "山田太郎", "加藤五郎" ]
        for (phoneNumber, label) in zip(phoneNumbers, labels) {
            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
        }

        // コンテキストへのリクエストを完了する。
        context.completeRequest()
    }
}

// MARK: - CXCallDirectoryExtensionContextDelegate
extension CallDirectoryHandler: CXCallDirectoryExtensionContextDelegate {
    func requestFailed(for extensionContext: CXCallDirectoryExtensionContext, withError error: Error) {
        // 必要に応じてエラーハンドリング
    }
}

CXCallDirectoryExtensionContextのデリゲートを設定するとデリゲートメソッドを通じてエラーが拾えるようになるので必要に応じてエラーハンドリングしてください。エラーハンドリングしない場合は不要です。

CXCallDirectoryExtensionContext のaddIdentificationEntry(withNextSequentialPhoneNumber:label:)メソッドを使って電話番号とラベルのセットを追加します。
ここで追加した電話番号から電話がかかってきた時に対応するラベルが表示されます。

最後にコンテキストへのリクエストを完了します。

発信者名表示を行うには着信拒否設定と着信IDの設定が必要

実装しても、発信者名表示を行うにはユーザーに発信者IDの提供を許可してもらう必要があります。 設定アプリの電話 > 着信拒否設定と着信IDから該当アプリをONにします。OFFになっていると発信者名が表示されないのでご注意ください。

プログラムから着信拒否設定と着信IDの設定が有効になっているかを確認する方法

着信拒否設定と着信IDの設定はプログラムからは不可能です。必ずユーザーに許可してもらう必要があります。 そうなると、現在の設定が有効になっているのか、無効になっているのかをプログラムから取得して、 無効になっている場合はユーザーに有効にしてもらうように何かしらのメッセージを表示する必要が出てきます。

プログラムから着信拒否設定と着信IDの設定が有効になっているかを確認するには、CXCallDirectoryManagergetEnabledStatusForExtension(withIdentifier:completionHandler:)メソッドを使います。

ステータスの取得は非同期で行われ、取得完了時の処理はバックグラウンドスレッド上で実行されます。UI操作を行う場合はご注意ください。

CXCallDirectoryManager
    .sharedInstance
    .getEnabledStatusForExtension(withIdentifier: <Call Directory ExtensionのバンドルID>) { (status, errorOrNil) in
        switch status {
        case .enabled:
        // 有効
        case .disabled:
        // 無効
        case .unknown:
            // 不明
        }
}

プログラムからCall Directory Extensionを読み込む方法

「着信拒否設定と着信IDの設定」をOFFからONにすると初回はExtensionが読み込まれます。 一方で、新しく電話帳データが追加された場合などに発信者名表示に反映させたい場合はプログラムからExtensionを読み込む必要があります。 読み込むにはCXCallDirectoryManagerreloadExtension(withIdentifier:completionHandler:)メソッドを使います。
Call Directory ExtensionのバンドルIDを指定して対象のExtensionを読み込みます。 エラーの場合はCXErrorCodeCallDirectoryManagerErrorが渡されます。 ちなみにバンドルIDが正しくない場合はnoExtensionFoundエラーが発生します。

読み込みは非同期で行われ、読み込み完了時の処理はバックグラウンドスレッド上で実行されます。UI操作を行う場合はご注意ください。

CXCallDirectoryManager
    .sharedInstance
    .reloadExtension(withIdentifier: <Call Directory ExtensionのバンドルID>) { errorOrNil in
        if let error = errorOrNil as? CXErrorCodeCallDirectoryManagerError {
            print("reload failed")

            switch error.code {
            case .unknown:
                print("error is unknown")
            case .noExtensionFound:
                print("error is noExtensionFound")
            case .loadingInterrupted:
                print("error is loadingInterrupted")
            case .entriesOutOfOrder:
                print("error is entriesOutOfOrder")
            case .duplicateEntries:
                print("error is duplicateEntries")
            case .maximumEntriesExceeded:
                print("maximumEntriesExceeded")
            case .extensionDisabled:
                print("extensionDisabled")
            case .currentlyLoading:
                print("currentlyLoading")
            case .unexpectedIncrementalRemoval:
                print("unexpectedIncrementalRemoval")
            }
        } else {
            print("reload succeeded")
        }
}

発信者名がちゃんと表示されるか確認する方法

発信者名がちゃんと表示されるか確認するには実際にその人の電話からかけてもらうのも1つの手ではありますが、 開発段階で確認するには自分一人で確認できた方が何かと都合が良いです。 そのためには、AppleがCallKitのサンプルコードとして提供しているSpeakerboxが便利です。このアプリを使うと着信をシミュレートすることができます。

アプリを起動したら「Simulate Incoming Call...」をタップします。

電話番号を入力し、Doneボタンをタップします。

以下のように着信画面が表示されます。ちゃんと発信者名が表示されていますね!!

ハマりどころ

実装上、ハマりどころと思われるところをご紹介します。

電話番号は国番号始まりの形式じゃないとダメ

以下、CallKitのドキュメントからの引用です。

Phone numbers in a Call Directory extension are represented by the CXCallDirectoryPhoneNumber type and consist of a country calling code (such as 1 for North America or 86 for China) followed by a sequence of digits.

CXCallDirectoryPhoneNumber型の電話番号は国番号始まりの形式である必要があります。 例えば、日本だと国番号は81なので、電話番号が08011111111の場合は818011111111となります。 この形式になっていなくてもエラーにはなりませんが、発信者名が表示されないので注意しましょう。

登録の際、電話番号が昇順になっていないとダメ

電話番号とラベルのセットを登録する際、電話番号の昇順に登録しないとCXErrorCodeCallDirectoryManagerError.entriesOutOfOrderエラーが発生します。ご注意ください。

登録の際、重複した電話番号があるとダメ

電話番号とラベルのセットを登録する際、重複した電話番号があるとCXErrorCodeCallDirectoryManagerError.duplicateEntriesエラーが発生します。エラーにならないようにするにはSetなどを使って重複を取り除くようにしましょう。

多数のデータを登録する場合は適切なメモリ解放が必要

以下は100万件のデータを登録するサンプルコードです。
このように多数のデータを登録する場合は適宜autoreleasepoolを使ってメモリを解放しましょう。
autoreleasepoolを使わないとメモリ不足でCXErrorCodeCallDirectoryManagerError.loadingInterruptedエラーが発生して処理が中断してしまいます。

let count = 1000000
for i in 1...count {
    // autoreleasepoolを使ってループ毎にメモリを解放
    autoreleasepool {
        context.addIdentificationEntry(withNextSequentialPhoneNumber: CXCallDirectoryPhoneNumber(i),
                                       label: "Name" + String(i))
    }
}

iOS 11から差分データの提供が可能になった

iOS 11から差分データの提供が可能になりました。(iOS 10では全件登録しかAPIが用意されてなかった) CXCallDirectoryExtensionContextに追加された関連プロパティ、メソッドは以下です。

以下のようにCXCallDirectoryExtensionContextのisIncrementalプロパティがtrueの場合は差分を、falseの場合は全件分のデータを登録します。
removeIdentificationEntry(withPhoneNumber:)removeAllIdentificationEntries()isIncrementalがtrueの時しか使えないのでご注意ください。

if context.isIncremental {
    // 差分を登録する
} else {
    // 全件を登録する
}

おわりに

今回はCall Directory Extensionを使った発信者名表示の実装方法をご紹介しました。 Webを検索してもあまり情報が無かったので記事にしてみました。 どなたかの参考になれば幸いです。

参考