https://d226lax1qjow5r.cloudfront.net/blog/blogposts/handling-voip-push-notifications-with-callkit/callkit-push-notifications.png

iOSのCallkitを使用してVoIPプッシュ通知を処理する方法

最終更新日 May 25, 2023

所要時間:1 分

このチュートリアルでは CallKitを使用してiOSデバイスに送信されるVoIPプッシュ通知を処理します。 iOS用Vonage Client SDKを使用する際に、iOSデバイスに送信されるVoIPプッシュ通知を処理するためにCallKitを使用します。.VoIPプッシュ通知は、アプリケーションがフォアグラウンドであるかどうかに関係なくデバイスに到達するため、Client SDKで着信コールを受信するための主要な方法です。CallKitを使用すると、iOSアプリケーションをシステムに統合できるため、アプリケーションをネイティブのiOS電話のように見せることができます。Callkit で Vonage Client SDK を使用することで、着信コールのための一貫した使い慣れたインターフェイスを持ちながら、アプリケーションに通話を組み込むことができます。

前提条件

  • Vonage APIアカウント。まだお持ちでない方は 今すぐサインアップ.

  • Apple DeveloperアカウントとiOSデバイス(またはXcode 14以上のシミュレータ)。

  • GitHubアカウント。

  • Xcode 12とSwift 5以上。

  • ココアポッドをクリックしてVonage Client SDK for iOSをインストールしてください。

  • コマンドラインインターフェイスです。でインストールできます。 npm install -g @vonage/cli.

スターター・プロジェクト

このブログは "アプリ内で電話を受ける"の上に構築していきます。このチュートリアルは、チュートリアルプロジェクトの完成した状態から始めます。このチュートリアルに従うか、すでにVonage Client SDK音声アプリケーションの構築に慣れている場合は、GitHubからスタータープロジェクトをクローンしてください。 プロジェクトをクローンしてください。.

プッシュ証明書の設定

iOSアプリで使えるプッシュ通知には、PushKitを使ったVoIPプッシュとユーザー通知の2種類があります。このチュートリアルでは、VoIPプッシュに焦点を当てます。 Appleプッシュ通知サービス(APNs)は、APNsとVonageサーバー間の接続を保護するために証明書ベースの認証を使用します。そのため、証明書を作成してVonageサーバーにアップロードし、着信があったときにVonageがデバイスにプッシュを送信できるようにする必要があります。

プッシュ通知機能の追加

プッシュ通知を使用するには、Xcodeプロジェクトにプッシュ通知機能を追加する必要があります。環境設定から Xcode の Apple 開発者アカウントにログインしていることを確認してください。そうであれば、ターゲットを選択し 署名と機能:

signing and capabilities tag

次に機能の追加を選択し プッシュ通知バックグラウンド・モードケイパビリティを追加します:

add capability button

Background Modes機能で ボイスオーバーIPそして バックグラウンド処理.Xcode が自動的にアプリの署名を管理している場合、バンドル識別子にリンクされているプロビジョニング・プロファイルを更新し、機能を含めるようにします。

VoIPプッシュ通知を使用する場合は、CallKitフレームワークを使用する必要があります。これをプロジェクトにリンクするには フレームワーク、ライブラリ、組み込みコンテンツの下に追加します:

add callkit framework

プッシュ証明書の生成

プッシュ証明書を生成するには、Appleデベロッパーアカウントにログインし、以下のページにアクセスする必要があります。 証明書、識別子、プロファイルページにアクセスし、新しい証明書を追加します:

add certificate button

選択 Appleプッシュ通知サービスSSL(サンドボックス&プロダクション)を選択し、次に進みます。

Certificate wizard checkbox

VoIPプッシュ通知を追加したいアプリのApp IDを選択し、次に進みます。アプリがリストにない場合は、App IDを作成する必要があります。Xcodeが自動的に署名を管理している場合は、Xcodeがこれをやってくれます。そうでない場合は、"Certificates, Identiers & Profiles "で新しいApp IDを作成することができます。 証明書、識別子とプロファイルページで新しい App ID を作成することができます。その際、必ずプッシュ通知機能を選択してください。

証明書署名要求(CSR)をアップロードするよう促されます。アップル社のヘルプサイト アップルのヘルプサイトの指示に従ってMacでCSRを作成してください。CSRがアップロードされると、証明書をダウンロードできるようになります。そのファイルをダブルクリックして .cerファイルをダブルクリックしてKeychain Accessにインストールします。

プッシュ証明書をVonageサーバーで必要な形式で取得するには、それをエクスポートする必要があります。キーチェーンアクセスでVoIPサービス証明書を探し、右クリックしてエクスポートします。エクスポートの名前を applecertを選択します。 .p12を選択します:

keychain access export

プッシュ証明書のアップロード

あなた アップロードあなたの 証明書を ボンテージ にアップロードします。 APIダッシュボードを使用してVonageに証明書をアップロードします。アプリケーションを ダッシュボードを開き、"Enable プッシュ通知 プッシュ通知" タブを開く:

Push Upload on the dashboardPush Upload on the dashboard

証明書をアップロードし .p12をアップロードし、必要に応じてパスワードを追加します。

ClientManagerクラス

新しいSwiftファイル(CMD + N)を作成し ClientManager.このクラスは、今後のステップで複数の場所で Client SDK から情報を取得する必要があるため、Client SDK とのインターフェースに必要なコードをカプセル化します:

で置き換える。 ALICE_JWT本番環境では、ここで認証サーバー/エンドポイントからJWTを取得します。

この新しいクラスでは、Client SDKの呼び出しコードを ViewControllerクラスから ClientManagerクラスに移動する必要があります。この2つのクラスは ClientManagerDelegateオブザーバと通信します。以下の変更を ViewControllerクラスに以下の変更を加えます:

class ViewController: UIViewController {
    
    private let connectionStatusLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        ClientManager.shared.delegate = self
        
        connectionStatusLabel.text = ""
        connectionStatusLabel.textAlignment = .center
        connectionStatusLabel.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(connectionStatusLabel)
        
        view.addConstraints([
            connectionStatusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            connectionStatusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
}

extension ViewController: ClientManagerDelegate {
    func clientStatusUpdated(_ clientManager: ClientManager, status: String) {
        DispatchQueue.main.async {
            self.connectionStatusLabel.text = status
        }
    }
}

プッシュ通知の登録

次のステップは、Vonageにどのユーザーのどのデバイスにプッシュ通知を送るかを知らせるために、プッシュ通知用のデバイスを登録することです。そのために ClientManagerクラスに pushTokenプロパティと以下の関数を追加して、デバイスのプッシュトークンを処理します:

final class ClientManager: NSObject {
    public var pushToken: Data?
    weak var delegate: ClientManagerDelegate?
    ...

    func invalidatePushToken(_ completion: (() -> Void)? = nil) {
        print("VPush: Invalidate token")
        if let deviceId = UserDefaults.standard.object(forKey: Constants.deviceId) as? String {
            client.unregisterDeviceTokens(byDeviceId: deviceId) { error in
                if error == nil {
                    self.pushToken = nil
                    UserDefaults.standard.removeObject(forKey: Constants.pushToken)
                    UserDefaults.standard.removeObject(forKey: Constants.deviceId)
                    completion?()
                }
            }
        } else {
            completion?()
        }
    }

    private func registerPushIfNeeded(with token: Data) {
        shouldRegisterToken(with: token) { shouldRegister in
            if shouldRegister {
                self.client.registerVoipToken(token, isSandbox: true) { error, deviceId in
                    if error == nil {
                        print("VPush: push token registered")
                        UserDefaults.standard.setValue(token, forKey: Constants.pushToken)
                        UserDefaults.standard.setValue(deviceId, forKey: Constants.deviceId)
                    } else {
                        print("VPush: registration error: \(String(describing: error))")
                        return
                    }
                }
            }
        }
    }

    private func shouldRegisterToken(with token: Data, completion: @escaping (Bool) -> Void) {
        let storedToken = UserDefaults.standard.object(forKey: Constants.pushToken) as? Data
        
        if let storedToken = storedToken, storedToken == token {
            completion(false)
            return
        }
        
        invalidatePushToken {
            completion(true)
        }
    }

この registerPushIfNeeded関数はトークンを受け取り shouldRegisterToken関数を使って、そのトークンがすでに登録されているかどうかをチェックする。もし登録されていなければ registerVoipTokenクライアントはVonageにプッシュ通知を登録します。この AppDelegateクラスで、VoIPプッシュ通知を登録できるようになりました。インポート PushKitをファイルの先頭に追加します:

import PushKit

のローカルインスタンスを追加する。 ClientManagerクラスのローカルインスタンスを追加します:

class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
    private let clientManager = ClientManager.shared
    ...
}

プッシュ通知用にデバイスを登録する関数を含む、新しい拡張子をファイルの最後に作成します:

extension AppDelegate: PKPushRegistryDelegate {
    func registerForVoIPPushes() {
        let voipRegistry = PKPushRegistry(queue: nil)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [PKPushType.voIP]
    }
}

を更新する。 didFinishLaunchingWithOptions関数を更新して registerForVoIPPushes関数を呼び出し、Client SDKにログインします:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    AVAudioSession.sharedInstance().requestRecordPermission { (granted:Bool) in
        print("Allow microphone use. Response: \(granted)")
    }
    registerForVoIPPushes()
    clientManager.login()
    return true
}

拡張機能に PKPushRegistryDelegateプッシュ通知の登録を処理する関数をエクステンションに追加します:

extension AppDelegate: PKPushRegistryDelegate {
    ...

    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
        clientManager.pushToken = pushCredentials.token
    }

    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        clientManager.invalidatePushToken(nil)
    }
}

プッシュトークンは ClientManagerクラスのプロパティとして保存されます。 login関数を ClientManagerクラスの関数を編集してください:

func login(isPushLogin: Bool = false) {
    print("VPush: Login - isPush:", isPushLogin)
    guard !isActiveCall else { return }
    
    ongoingPushLogin = isPushLogin
    
    getJWT { jwt in
        self.client.createSession(jwt) { error, sessionID in
            let statusText: String
            if error == nil {
                statusText = "Connected"
                
                if isPushLogin {
                    self.handlePushLogin()
                } else {
                    self.handleLogin()
                }
            } else {
                statusText = error!.localizedDescription
            }
            
            self.delegate?.clientStatusUpdated(self, status: statusText)
        }
    }
}

private func handlePushLogin() {
    ongoingPushLogin = false
    
    if let storedAction = storedAction {
        storedAction()
    }
}

private func handleLogin() {
    if let token = pushToken {
        registerPushIfNeeded(with: token)
    }
}

着信プッシュ通知の処理

デバイスが登録されると、Vonageからのプッシュ通知を受信できるようになります。Client SDKには、プッシュ通知のペイロードが期待されるペイロードかどうかをチェックし、ペイロードを処理する機能があります。そして processCallInvitePushDataの関数で受信される呼び出しに変換します。 didReceiveInviteForCallの関数で受信される呼び出しに変換します。 VGVoiceClientDelegate.

プッシュトークンの登録と同様に、Client SDKがログインしているときにのみ、受信したプッシュを処理します。の関数を実装します。 ClientManagerクラスの関数を実装します:

final class ClientManager: NSObject {
    ...

    private var ongoingPushLogin = false
    private var ongoingPushKitCompletion: () -> Void = { }
    private var storedAction: (() -> Void)?

    ...

    func isVonagePush(with userInfo: [AnyHashable : Any]) -> Bool {
        VGVoiceClient.vonagePushType(userInfo) == .unknown ? false : true
    }

    func processPushPayload(with payload: [AnyHashable : Any], pushKitCompletion: @escaping () -> Void) -> String? {
        self.ongoingPushKitCompletion = pushKitCompletion
        return client.processCallInvitePushData(payload)
    }

    ...
}

には PKPushRegistryDelegateというプッシュがあったときに呼び出される関数がある。 didReceiveIncomingPushWithエクステンション PKPushRegistryDelegateに追加する。 AppDelegate.swiftファイルに追加する:

func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
    if clientManager.isVonagePush(with: payload.dictionaryPayload) {
        clientManager.login(isPushLogin: true)
        _ = clientManager.processPushPayload(with: payload.dictionaryPayload, pushKitCompletion: completion)
    }
}

VoIPプッシュ通知の着信があった場合は、ログインすることをお勧めします。 loginが呼ばれる理由です。これは ClientManagerクラスのロジックを使用します。このロジックは後の段階で実装されます。

iOSアプリケーションにVoIPプッシュ通知の着信があった場合、CallKitフレームワークの CXProviderクラスを使用して処理しなければなりません。という名前の新しい Swift ファイル (CMD + N) を作成します。 ProviderDelegate:

import CallKit
import AVFoundation
import VonageClientSDKVoice

final class ProviderDelegate: NSObject {
    private let provider: CXProvider
    private let callController = CXCallController()
    private var activeCall: UUID? = nil
    
    override init() {
        provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
        super.init()
        provider.setDelegate(self, queue: nil)
    }
    
    static var providerConfiguration: CXProviderConfiguration = {
        let providerConfiguration = CXProviderConfiguration()
        providerConfiguration.maximumCallsPerCallGroup = 1
        providerConfiguration.supportedHandleTypes = [.generic, .phoneNumber]
        return providerConfiguration
    }()
}

この物件は callControllerプロパティは CXCallControllerオブジェクトです。次に、ファイルの最後に拡張子を作成して CXProviderDelegate:

extension ProviderDelegate: CXProviderDelegate {
   func providerDidReset(_ provider: CXProvider) {
        activeCall = nil
    }

    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        ClientManager.shared.answer(activeCall!.uuidString.lowercased()) { error in
            if error == nil {
                action.fulfill()
            } else {
                action.fail()
            }
        }
    }
    
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        hangup(action: action)
    }
    
    func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        VGVoiceClient.enableAudio(audioSession)
    }
    
    func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        VGVoiceClient.disableAudio(audioSession)
    }
}

CallKit がオーディオセッションを有効化/無効化するとき、デリゲート関数は CallKit オーディオセッションを使用して Client SDK オーディオを有効化/無効化します。

CallKit UIが呼び出しに応答すると、デリゲート関数を呼び出します。 CXAnswerCallActionデリゲート関数を呼び出します。これは answer関数を呼び出します。 ClientManagerに実装する関数を呼び出します。

CXEndCallActionを呼び出す CallKit UI から通話が終了したときに呼び出されます。 hangup関数を呼び出します。で必要な残りの関数を実装します。 ProviderDelegateクラスに必要な残りの関数を実装します:

final class ProviderDelegate: NSObject {
    ...
    
    func reportCall(_ callID: String, caller: String, completion: @escaping () -> Void) {
        activeCall = UUID(uuidString: callID)
        let update = CXCallUpdate()
        update.localizedCallerName = caller
        
        provider.reportNewIncomingCall(with: activeCall!, update: update) { error in
            if error == nil {
                completion()
            }
        }
    }
    
    func didReceiveHangup(_ callID: String) {
        let uuid = UUID(uuidString: callID)!
        provider.reportCall(with: uuid, endedAt: Date.now, reason: .remoteEnded)
    }
    
    func reportFailedCall(_ callID: String) {
        let uuid = UUID(uuidString: callID)!
        provider.reportCall(with: uuid, endedAt: Date.now, reason: .failed)
    }
    
    private func hangup(action: CXEndCallAction) {
        if activeCall == nil {
            endCallTransaction(action: action)
        } else {
            ClientManager.shared.reject(activeCall!.uuidString.lowercased()) { error in
                if error == nil {
                    self.endCallTransaction(action: action)
                }
            }
        }
    }
    
    private func endCallTransaction(action: CXEndCallAction) {
        self.callController.request(CXTransaction(action: action)) { error in
            if error == nil {
                self.activeCall = nil
                action.fulfill()
            } else {
                action.fail()
            }
        }
    }
}

を呼び出します。 reportCall関数呼び出し reportNewIncomingCallは CallKit システムの UI をトリガーし、他の関数はコールを更新または終了するのに役立ちます。これで ProviderDelegateクラスが完成したので ClientManagerクラスを更新します。クライアントマネージャに providerDelegateプロパティを追加します:

final class ClientManager: NSObject {
    ...

    private let providerDelegate = ProviderDelegate()

    ...
}

次に VGVoiceClientDelegate:

extension ClientManager: VGVoiceClientDelegate {
    func voiceClient(_ client: VGVoiceClient, didReceiveInviteForCall callId: VGCallId, from caller: String, with type: VGVoiceChannelType) {
        print("VPush: Received invite", callId)
        providerDelegate.reportCall(callId, caller: caller, completion: ongoingPushKitCompletion)
    }
    
    func voiceClient(_ client: VGVoiceClient, didReceiveHangupForCall callId: VGCallId, withQuality callQuality: VGRTCQuality, reason: VGHangupReason) {
        print("VPush: Received hangup")
        isActiveCall = false
        providerDelegate.didReceiveHangup(callId)
    }
    
    func voiceClient(_ client: VGVoiceClient, didReceiveInviteCancelForCall callId: String, with reason: VGVoiceInviteCancelReason) {
        print("VPush: Received invite cancel")
        providerDelegate.reportFailedCall(callId)
    }
    
    func client(_ client: VGBaseClient, didReceiveSessionErrorWith reason: VGSessionErrorReason) {
        let reasonString: String!
        
        switch reason {
        case .tokenExpired:
            reasonString = "Expired Token"
        case .pingTimeout, .transportClosed:
            reasonString = "Network Error"
        default:
            reasonString = "Unknown"
        }
        
        delegate?.clientStatusUpdated(self, status: reasonString)
    }
}

SDKがコールを処理した後、デリゲート関数のコールインビテーションを取得します。 didReceiveInviteForCallデリゲート関数にコールの招待が送られ、デリゲート関数がコールを報告します。 didReceiveHangupForCallそして didReceiveInviteCancelForCallまた、プロバイダデリゲート上でそれぞれの関数を呼び出します。クラスを完成させるには ClientManagerクラスを完成させるには answerreject関数を追加します:

func answer(_ callID: String, completion: @escaping (Error?) -> Void) {
    let answerAction = {
        print("VPush: Answer", callID)
        self.isActiveCall = true
        self.client.answer(callID, callback: completion)
    }
    
    if ongoingPushLogin {
        print("VPush: Storing answer")
        storedAction = answerAction
    } else {
        answerAction()
    }
    
}

func reject(_ callID: String, completion: @escaping (Error?) -> Void) {
    let rejectAction = {
        print("VPush: Reject", callID)
        self.isActiveCall = false
        self.client.reject(callID, callback: completion)
    }
    
    if ongoingPushLogin {
        print("VPush: Storing Reject")
        storedAction = rejectAction
    } else {
        rejectAction()
    }
}

この場合も、Client SDK がログインした後でなければ、通話に応答したり拒否したりできません。どちらの関数も ongoingPushLoginを使用してログインが正常に完了したかどうかをチェックします。 storedAction.関数を見ると handlePushLogin関数を見ると、ログインが完了すると、保存されているアクションがあればそれを呼び出すことがわかります。

試してみる

プロジェクトをiOSデバイス(Xcode 14以上ではシミュレーター)にビルドして実行(CMD + R)し、マイクの許可を得てデバイスをロックします。そして、先ほどのVonageアプリケーションにリンクされている番号に電話をかけます。ロック画面に直接着信が表示され、電話に出ると、おなじみのiOSの通話画面になります:

incoming call with locked screen

active call from locked screen

端末の通話履歴を確認すると、そこに通話が記録されているのがわかる。

次はどうする?

完成したプロジェクトは GitHub.Client SDKとCallKitを使うことで、さらに多くのことができるようになります。Client SDKの詳細については以下をご覧ください。 Vonage Client SDKの概要およびCallKitについては developer.apple.comでご覧いただけます。.

シェア:

https://a.storyblok.com/f/270183/400x400/19c02db2d3/abdul-ajetunmobi.png
Abdul AjetunmobiVonage 元チームメンバー

アブドゥルはVonageのデベロッパー・アドボケイト。iOSエンジニアとして消費者向け製品に携わった経歴を持つ。余暇には、サイクリング、音楽鑑賞、技術者としての道を歩み始めたばかりの人々の指導を楽しんでいる。