CallKit の概要と Call Directory Extension でできること,できないこと


はじめに

iOS10 から,電話の機能をオーバーライドできる API,CallKit が開放されました.ただ,開放されてから1年以上経つものの,未だにあまり知られておらず,いまいち何ができるかよく分からないといった方も多いかと思いますので,ざっくりと概要を書いてみようと思います.

また,App Extension として,着信拒否したり発信者識別できたりする Call Directory Extension も開放されており,実際に使ってみたところ,何ができて何ができなかったのかも分かったので,そちらも併せて書こうと思います.

CallKit の概要

CallKit は,iOS の VoIP 機能に,アプリから直接アクセスすることのできるフレームワークです.これにより,「電話」アプリと同様のインタフェースや,高音質な通話機能を簡単に実現できるようになりました.また,CallKit を利用することで,アプリ内でおこなった通話が「電話」アプリの通話履歴に表示されるようになり,逆に「電話」アプリから対象のアプリを指定して電話をかけることもできるようになりました.Android ではこれらのことは従来より実現可能でしたが,iOS でもようやく開放されたことになります.

CallKit のインパクトは,通話機能をもつ既存アプリにとっては大きく,Facebook や Skype などが iOS10 のリリースとほぼ同時に CallKit に対応したアップデートをおこないました.国内では LINE が,リリースから2ヶ月経った昨年11月にようやく CallKit に対応しております.なぜ既存の大手企業が対応を急いだのか,逆に言えば,なぜ既存の通話機能のままではダメだったのかというと,その理由は 着信時の UX にありました.

従来は,アプリに着信した際,通常のプッシュ通知がきて,それをタップし,さらに iPhone のロック解除をしてはじめて通話ができる仕組みになっていました (もう忘れてる人も多いかもしれません).しかし iOS10 からは,PushKit (VoIP プッシュ) により CallKit の機能を呼び出すことができるようになりました.すると,着信時にプッシュ通知ではなく「電話」アプリと同様の着信画面が表示され,さらにロックされた状態のままで出ることができるようになりました.既存の仕組みだと,通話するまでに面倒な作業が多く,電話を取り損ねてしまうケースも少なくなかったですが,PushKit と CallKit によって,着信の UX が劇的に改善されました.これが既存企業の CallKit 対応に拍車をかけたように思われます.

このように,「電話」アプリとほぼ同じ UX を簡単に実現できるようになりましたが,これらを実現するためにアプリで実装するものは,大きく分けて CXProviderCXCallController の2つです.以下は WWDC2016 のスライドを抜粋したものです.

CXProvider

通話のインタフェースをここに定義します.通話画面のインタフェースはもちろん,ビデオ通話の ON/OFF や,通話人数なども以下の様に定義します.

let providerConfiguration = CXProviderConfiguration(localizedName: "Wacall")
providerConfiguration.supportsVideo = false  // ビデオ通話
providerConfiguration.maximumCallsPerCallGroup = 1  // 最大通話人数
providerConfiguration.supportedHandleTypes = [.phoneNumber]  // 扱うタイプ

provider = CXProvider(configuration: type(of: self).providerConfiguration)    
provider.setDelegate(self, queue: nil)

また,着信,発信,切断,保留等の,通話における各アクションの挙動も CXProviderDelegate のメソッドとして定義します.以下は着信時の例です.

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
  guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
    action.fail()
    return
  }
  // ここに着信時の処理
}

CXCallController

システムに発信,切断等のリクエストをここで通知します.以下では発信リクエストのトランザクションを生成しています.

func startCall(handle: String, videoEnabled: Bool) {
  let handle = CXHandle(type: .phoneNumber, value: handle)
  let startCallAction = CXStartCallAction(call: UUID(), handle: handle)  // 発信のアクション
  startCallAction.isVideo = videoEnabled
  let transaction = CXTransaction(action: startCallAction)  
  requestTransaction(transaction)
}

Call Directory Extension でできること,できないこと

Call Directory Extension は,VoIP 通話機能に対して,あらかじめアプリで登録したリストをもとに着信拒否や発信者識別をおこなうことができる Extension です.こちらの機能を利用したい方の方が多いかもしれません.

先日作った,着信時に迷惑電話かどうかがリアルタイムに分かるアプリ Wacall をベースに説明します.

Call Directory Extension を利用しているアプリがあると,「設定」→「電話」→「着信拒否設定と着信ID」にそのアイコンがあらわれます.そして ON にすると,電話がかかってくるたびに,その電話番号を登録されているリストとマッチングさせて,もしリスト内に存在すれば,着信拒否または発信者識別 (あらかじめ登録していたラベルを表示) をおこないます.上の例では発信者識別をおこなっており (ラベルはすべて「⚠️迷惑電話の可能性」),通話履歴にも表示されています.Extension 内のコードは以下のようになっています.

private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
        // Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
        // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
        //
        // Numbers must be provided in numerically ascending order.

        do {
            let numbersString = try String(contentsOfFile: Bundle.main.path(forResource: "numbers", ofType: "txt")!, encoding: .utf8)  // numbers.txt が,登録された迷惑電話リスト
            let phoneNumbers: [CXCallDirectoryPhoneNumber] = numbersString.components(separatedBy: .newlines).flatMap { Int64($0) }
            let labels = [String](repeating: "⚠️迷惑電話の可能性", count: phoneNumbers.count)
            for (phoneNumber, label) in zip(phoneNumbers, labels) {
                context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
            }
        } catch let error as NSError {
            print(error.localizedDescription)
        }
    }

登録されているリストは,以下のようにアプリ内からリロードすることもできます.電話番号リストを定期的にアップデートする時などに利用できます.

CXCallDirectoryManager.sharedInstance.reloadExtension(withIdentifier: "tokyo.pikashi.Wacall.WacallDirectory", completionHandler: nil)

また,電話番号リストは あらかじめ登録されている 必要があります.なので,かかってきた電話番号をもとに問い合わせをおこなう,のようなことはできません.これはセキュリティ上当然といえば当然です.

おわりに

iOS10 から開放された CallKit と Call Directory Extension に関してざっくりとまとめてみました.

利用シーンが限定的なためドキュメントもあまり充実していないので,CallKit を利用する際はぜひ参考にしてください.