くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Callkitで着信画面を表示させる

今回はCallkitについてです

Callkitは下記のようなiPhone標準の着信画面を表示させる機能です。画面がOFFの状態であれば、スライドして応答する画面が表示されます。また、着信履歴にもデータが残ります

SkypeやLINEなどで電話をかけたときに、着信時に同じ画面が表示されていると思います。それはCallKitというiOSの公式フレームワークを使っています。

着信側の端末はPushKitと呼ばれるライブラリによってPush通知を受信し、これをトリガーにCallKitと呼ばれるライブラリを利用して着信画面を表示します。

iOS13以降からVoIP Push (PushKit)は着信通知専用に仕様変更されたことにより、PushKitとCallKit はセットで使うことが必須となりました。

ただ、単純に該当のメソッドを呼び出せばいいわけではありません

正しい順番を守って呼び出さないと、システム側がPushKitの呼び出しに失敗したと判断され、着信が来なくなることがあります

着信画面を表示させるためには以下の手順を踏む必要があります

  1. PushKitからVoIPのPushを受け取る
  2. CallKit 着信画面を表示
  3. reportNewIncomingCallのcompletionHandler内部でPushKit のcompletionHandlerを呼び出す

PushKitの設定

まずはPushKitで通知を送る設定が必要になります

VoIPのPushを受け取るためには、基本的にはAPNS通知を行うときと同じですが、Apple DeveloperでCertificateを作るときにVoIP用のチェックボックスがあるので、チェックして証明書を作成します

そして、Xcodeの設定で、Signinig&Capabilityの下記にチェックを入れましょう

また、ペイロードの設定などに注意しましょう。下記に記載されていますが、Sandboxの場合に届かなかったりするので注意が必要です。

developer.apple.com

ここまで設定するとPushKitの通知が送れるはずです。

PushKitの通知を受け取る処理は下記になります

extension AppDelegate {
    
    private func initVoIP() {
        var voipRegistry = PKPushRegistry(queue: .main)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [.voIP]
    }
    
}

extension AppDelegate: PKPushRegistryDelegate {

    // PushKitからVoIPPush用のトークンを受け取る
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type{
}

    // VoIPPushを受け取ったときの処理
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        // 通知の形式が .voIP の場合に後続の処理を行う
        guard type == .voIP else {
            completion()
            return
        }
        
        let userInfo = payload.dictionaryPayload
        
        // Callkitを表示させる処理を行う
    }
}

PKPushRegistryDelegateでPushKitからVoIPPushを受け取り、Callkitを表示させる処理を行います

基本的に上記の処理はAppDelegateで行います

ここまでで着信のPush通知が届くので、次に着信画面を表示させる流れになります

Callkitの設定

次に着信画面を表示させるCallkitの処理を行います

まずはCallkitの初期化を行います。今回はシングルトンで作成しています

import CallKit

final class CallKitManager {
    
    private(set) var callKitProvider: CXProvider!
    private(set) var callKitController = CXCallController()
    var uuid = UUID()
    
    static let shared = CallKitManager()
    
    private init() {
        let configuration = CXProviderConfiguration(localizedName: "TEST".localize())
        configuration.maximumCallGroups = 1
        // 最大通話人数
        configuration.maximumCallsPerCallGroup = 1
        // 扱うタイプ
        providerConfiguration.supportedHandleTypes = [.phoneNumber] 
        // 着信音の設定
        configuration.ringtoneSound = "〇〇.mp3"
        callKitProvider = CXProvider(configuration: configuration)
    }
}

CXCallControllerは通話の管理インタフェースを提供しているクラスです。

developer.apple.com

CXProviderConfigurationは挙動や表示を制御するためのプロパティです

developer.apple.com

localizedNameは着信で表示されるアプリ名を設定します

次に着信処理を行います。

// 通話相手の名前設定
let callHandle = CXHandle(type: .generic, value: "TEST君")

let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
// 他の着信が来たら受け取るかどうか
callUpdate.supportsHolding = false
// グループ通話を行うかどうか
callUpdate.supportsGrouping = false
// ビデオ通話を行うかどうか
callUpdate.hasVideo = false

provider.setDelegate(self, queue: .main)
 provider.reportNewIncomingCall(with: CallKitManager.shared.uuid, update: callUpdate, completion: {_ in})

CXProviderクラスのreportNewIncomingCallが着信画面を表示するAPIです。

CXHandle のインスタンスに発信者の情報をセットします

電話番号やメールアドレスなどを指定すると、標準の連絡先アプリに登録されている情報を元に発信者名の表示や、画像が登録されていれば着信画面に表示されます

また、reportNewIncomingCallの先頭の引数に通話毎にユニークな uuid を渡しています

着信・発信・拒否・保留等の通話における各アクションの挙動はCXProviderDelegateのメソッドとして定義します(Callkitを表示させるViewcontrollerで行います)

extension CallKitViewController: CXProviderDelegate {
    // リセット時
    func providerDidReset(_ provider: CXProvider) {
        action.fail()
    }
    
    // 通話終了・着信を拒否した時
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill(withDateEnded: Date())
    }
    
    // タイムアウト時
    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {        
        action.fulfill()
    }
    
    // 発信時
    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    }
    
    // 着信を許可した時
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }
}

action.fulfill()は着信を受け取ったり拒否したりのアクションを終了させる処理です

通話発信や終了などを行う場合

通話発信や終了などを行う場合にはCXTransactionにアクションのインスタンスを登録してリクエストする必要があります

developer.apple.com

let endCallAction = CXEndCallAction(call: CallKitManager.shared.uuid)
 let transaction = CXTransaction(action: endCallAction)

CallKitManager.shared.callKitController.request(transaction) { error in
      if let _ = error {
          CallKitManager.shared.callKitProvider.reportCall(with: CallKitManager.shared.uuid, endedAt: nil, reason: .remoteEnded)
      }
}

上記は通話終了のアクションです

iOSでは標準の電話アプリの通話も含めてCXCallControllerで管理されるため、インスタンスを経由して発信や終話の状態をOSに通知する必要があります

OSに通知するというのはrequest(transaction)で行います

通話を開始する場合には上記のCXEndCallActionCXStartCallActionに変更します

まとめ

最低限しか記載していませんが、通話アプリの着信画面ではこんなことが行われているという大枠は理解できると思います

今回記載したこと以外に、着信履歴からリダイアルなど標準の電話とほとんど同じ機能が実現できるはずなので試してみてください!!