iOS13 (Xcode11) プッシュ機能変更のあれこれ

はじめに

こんにちは、初投稿になります!医療チームの山口です。
(ちなみに2020新卒入社の山口さんとは別の山口ですヨ!)

業務的には、主に医療チームのクライアント(Android, iOS)開発を担当しています。
ちなみに、プライベートではちびっ子一人を育てながら開発するママなエンジニアです。
(最近はコロナの影響で在宅が増えた関係上、ちびっ子に社用端末やPCを破壊されないように奪い合いの日々を過ごしております...)

今回はオンライン診療ポケットドクターでも対応しています iOS13 のプッシュの仕様変更について、WWDC2019 *1 の見解含めて記載したいと思います。

VoIP Push は着信専用

オンライン診療ポケットドクターでは Apple Push Notification Service (APNs) を利用することで通知表示や着信等の Push 機能を実装しています。
特に医療機関から一般ユーザーに対してビデオ通話を開始する際には、Voice-over-IP (VoIP) Push という通知機能を使って一般ユーザーの端末へ通知を投げます。
一般ユーザー側の端末は PushKit *2 と呼ばれるライブラリによってこのPushを受信し、これをトリガーに CallKit *3 と呼ばれるライブラリを利用して着信画面を表示します。
iOS13 以降から VoIP Push (PushKit) は着信通知専用に仕様変更されたことにより、PushKit と CallKit はセットで使うことが必須となりました。
着信以外の Push を VoIP Push で受け取ってローカルプッシュして通知に出している場合は、通常の Push に戻す必要があります。

また、"WWDC2019 App Background Execution *4" にて言及されていますが、バックグラウンドの制限が厳しくなりました。
先ほど PushKit と CallKit はセットで使うことが必須と記載しましたが、単純に該当のメソッドを呼び出せばいいのではなく、正しい順番を守って呼び出さないと、システム側が PushKit の呼び出しに失敗したと判断します。
特に iOS はアプリバックグランド時の制約が厳しく、この場合だとアプリがクラッシュします。
さらに呼び出し失敗を無視してアプリをクラッシュさせ続けると、最悪の場合システム側がPushを使えなくする強硬手段にでるようです。
(その他 iOS のバックグラウンド事情は 中村さんの記事 でも触れられていますね。)

要約すると「オレ様の指定したとおりに実装しろ、さもなくば使えなくしてやるぜっ!!」ということです。流石Apple様...

以下の順番で呼び出すようにしましょう。最終的にはキャプチャのようなCallKit着信画面が表示されます。

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

f:id:optim-tech:20200626134209p:plain:w200

Swift

// 1. PushKitからVoIPPushを受け取る。
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType,  completion: @escaping () -> Void) {

    let uuid = UUID()
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .generic, value: "optim_test")  // 第二引数は null を入れない
    update.localizedCallerName = "オプティム病院 オプティム太郎先生"  // 指定していないと「不明」と表示される
    update.hasVideo = true  // trueの場合「アプリ名ビデオ」、falseの場合「アプリ名オーディオ」と表示される

    // 2. CallKit着信画面を表示。
    callProvider.reportNewIncomingCall(with: uuid, update: update) { _ in
        // 3. CallKit#reportNewIncomingCall の completionHandler 内部で PushKit の completionHandler を呼び出す。
        completion()
    }
}

Objective-C

// 1. PushKitからVoIPPushを受け取る。
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {

    NSUUID *uuid =  [NSUUID UUID];
    CXCallUpdate *update = [[CXCallUpdate alloc] init];
    update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:@"optim_test"];
    update.localizedCallerName = @"オプティム病院 オプティム太郎先生";
    update.hasVideo = YES;

    // 2. CallKit着信画面を表示。
    [callProvider reportNewIncomingCallWithUUID:(NSUUID *)uuid update:(CXCallUpdate*)update completion:^(NSError * _Nullable error) {
        // 3. CallKit#reportNewIncomingCall の completionHandler 内部で PushKit の completionHandler を呼び出す。
        completion();
    }];
}

APNs Push Payload

こちらはサーバーサイドの話になりますが、APNs に定義されたヘッダーフィールドやペイロードを正しく設定しましょう。*5
特に apns-push-type は用途に合わせた値を正しく入れないと、APNsがエラーを返したり、Push が届くのに遅延が発生したり、Push を投げるリクエストを破棄したりするので注意しましょう。
apns-push-type を指定しなかった場合、APNs 開発環境の SANDBOX にて Push が来ないことを確認しています。)

要約すると「オレ様が作ったものは正しく使え、無視はするなっ!!」ということです。やっぱり流石Apple様...

尚、Amazon Simple Notification Service (SNS) では既に apns-push-type に対応しています。 *6

通知表示用のPush (Alert)

  • Header Field
    • apns-push-type に alert を指定
  • Push Payload
    • 以下は application(_:didReceiveRemoteNotification:fetchCompletionHandler:)userInfo で見ることができます。
{ 
    aps =     { 
        alert = "患者様からの予約が入りました。"; 
        "content-available" = 1; 
        sound = default; 
    };
    ...
} 

バックグラウンドで処理を行うためのPush (Background)

  • Header Field
    • apns-push-type に background を指定
  • Push Payload (ここ2つを対応しておかないとと、backgroundを指定したにも関わらず iOS12 以下で通知領域に表示されてしまうようなのでご注意を。)
    • content-available を 1 に指定
    • alert は空 (NULL) に指定
{ 
    aps =     { 
        alert = "<null>"; 
        "content-available" = 1; 
        sound = "<null>"; 
    };
    ...
}

着信用のPush (VoIP)

  • Header Field
    • apns-push-type に voip を指定
    • apns-expiration に 0 もしくは小さい値を指定 (これは先ほどのAppleDeveloper公式動画でも言及されています。)
  • Payload
    • 以下は pushRegistry(_:didReceiveIncomingPushWith:for:completion:)payload.dictionaryPayload で見ることができます。
{ 
    aps =     { 
        alert = "医療機関から着信がきています。"; 
        "content-available" = 1; 
        sound = "sound.aiff"; 
    }; 
    ...
}

終わりに

いかがでしたか?
iOSの着信周りは複雑で、メソッドを呼び出し損ねたりプッシュの設定に漏れがあったりすると、着信がこなかったりアプリが落ちたりしてしまいます。
また、すでに WWDC2020 *7 が開催されており、次期OS (iOS14) の新規機能や制約等が公開さています。
これからも 公式の仕様(Apple様のお達し)通りに実装するように心がけましょう!


オンライン診療ポケットドクターは 6/17 にバージョン 1.7.0 をリリースしました。
お手持ちのスマホの AppStore もしくは GooglePlay から無料でダウンロードすることが可能です。


オプティムではエンジニアを募集しています。