CallKitとPushKitを使ってアプリで電話を受け取る機能を作る


CallKitとPushKitを使ってアプリで電話を受け取る機能の全体感を掴むことが目的の記事です。

CallKitやPushKitはドキュメントが少なくて、それぞれどういう役割があって、どのように使うのか理解するのに時間がかかりました。ちなみに結局一番役に立ったのはAppleのドキュメントでしたね。。

今記事がCallKitを初めて使うことになった人の参考になれば幸いです。

CallKitとは

SkypeやLINEなどで電話をかけたときに、それがすべて同じUIを出していることに気づきました。無意識に使っていたのですが、それはCallKitというiOSの公式フレームワークを使っているからです。

思えば、LINE通話が電話の履歴に残るようになったタイミングがあったと思いますが、あれはiOS10からCallKitが使えるようになったからですね。

【開発者向け】iOS10で追加された新機能一覧

CallKitを使うと以下のようなUIをアプリから出すことができます。


図1. CallKitで出せる電話っぽいUI

しかも、これは電話アプリの履歴に残すことができます。1つのサードパーティの通話アプリが電話と同じような立ち位置になれることがCallKitの魅力ですね!


図2. iPhoneの電話の履歴

CallKitを理解するのに参考になった記事と読む時の注意

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

CallKitを日本語で調べると、CallKitExtensionが多くひっかかって混乱します。注意すべきなのはCallKitExtensionは連絡先を表示するときの名前の表示方法を変更するために使ったり、ブロックしたりするために使うプラスアルファの実装だという点です。本格的な通話アプリでなければ使わなくても大丈夫だと思います。

CallKit|Apple Developer Documentation

結局のところAppleのドキュメントが一番役に立ちます。最終的にはそこを確認しましょう。僕のこの記事も完全に信用すべきではないです。Apple Documentationが一番です。

CallKitとPushKitの関係

相手に電話をかけて、電話のUIを表示する」という機能を実現するためには、CallKitとPushKit、どちらか1つだけではダメで、どちらも使う必要があります。

LINE, Skype, HouseParty(アメリカで流行っている)がそれぞれどのように音声通話しているかは詳しくわかりませんが、アプリがやっている通話は、iPhoneどうしが電話番号to電話番号でやっている通話とは異なります。

それぞれがアプリの中で音声通話機能を実装しているだけです。CallKitを使うことで、その導入のインターフェースをiPhoneで通常行う電話と同じようにすることができるというだけです。

なので、先ほど貼った図1のようなUIを出すために何かトリガーが必要になります。そのトリガーがVoIPプッシュ通知であり、それを捌くために使うのがPushKitです。

つまり、

  • CallKit...電話っぽいUIや電話アプリの機能を少し借りるためのフレームワーク
  • PushKit...通話のトリガーとなるVoIPプッシュ通知を捌くためのフレームワーク

という認識です。

VoIP通知とは

詳しくはVoIPプッシュ通知(PushKit)と標準プッシュ通知の違いについてを見ていただけると良いと思いますが、通常実装するAPNSプッシュ通知との最大の違いは、アプリが起動していなくてもプッシュを処理するための実行時間が与えられるという点です。

つまり、VoIP通知を受け取ったアプリは、未起動でもコードを実行することができます。この間に電話のUIを出すコードを呼べば、たとえiPhoneがロック中であっても電話を受け取った感じでiPhoneを鳴らすことができるわけです。

iOSでVoIPプッシュ通知を実装するためにやることのフローは、基本的にはAPNS通知を行うときと同じですが、Apple DeveloperでCertificateを作るときにVoIP用のチェックボックスがあるので、それをチェックして作りましょう。

参考:プッシュ通知に必要な証明書の作り方2018

また、VoIPプッシュ通知を打つのは、APNS通知と同じようにサーバーです。APNS通知にAmazonSNSを使っていれば同じように実装できます。あたかも電話のように見えますが、デバイスtoデバイスを行なっているわけではなく、サーバーが何らかのトリガーでVoIPプッシュ通知を打っているにすぎません。

実際のAppDelegateでの実装

最後にAppDelegateにどのように書いてCallKitとPushKitを利用するかサンプルを載せます。

import PushKit
import CallKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    var provider: CXProvider?
    let pushRegistry = PKPushRegistry(queue: DispatchQueue.main)

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        // PushKit
        pushRegistry.delegate = self
        pushRegistry.desiredPushTypes = [.voIP]
        return true
    }
}

// MARK: - VoIP Push Notification
extension AppDelegate: PKPushRegistryDelegate {

    /// アプリ起動時に毎回呼ばれる(自分が観測した限りでは)
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        guard pushCredentials.token.count > 0 else {
            return
        }
        // ここでtokenが取れるので、プッシュ通知を打つサーバーに伝えましょう。
        print(pushCredentials.token)
    }

    /// VoIPプッシュ通知を受け取った時に呼ばれる
    /// アプリが起動していなくても発火します。
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
        shouldShowBringFriendsWhenStart = false
        guard type == .voIP else { return }

        // payloadのparseは独自実装です。
        if let data = try? JSONSerialization.data(withJSONObject: payload.dictionaryPayload, options: .prettyPrinted),
            let payload = try? JSONDecoder().decode(PushPayload.self, from: data),
            let uuid = payload.uuid {

            // MARK : - CallKit

            let configuration = CXProviderConfiguration(localizedName: "HogeApp")
            configuration.supportedHandleTypes = [.phoneNumber, .generic]

            self.provider = CXProvider(configuration: configuration)

            let update = CXCallUpdate()

            // これで図1の電話のUIが出ます。
            // typeは .phoneNumber, . emailAddress, .generic があります。
            // generalにいれたstringはかけてきた人の名前の部分に出せます。
            update.remoteHandle = CXHandle(type: .generic, value: name)
            self.provider!.reportNewIncomingCall(with: uuid, update: update,
                                                 completion: { error in

                if let error = error {
                    // エラーハンドリング
                } else {
                    // 通話につなぐための何かをする
                }
            })
        }
    }
}

上のコードは全体感をイメージするためのサンプルコードであり、そのままコピペしただけでは使えないので注意です。

まとめ

ってことで、CallKitとPushKitを使ってアプリで電話を受け取る機能の全体感を掴むための記事でした。

参考になれば幸いです!