🔨

iOS の Push 通知の種類

2021/07/14に公開

iOS の Push 通知は多少なり呼び方や特徴があるのでまとめました。また、それぞれに必要な設定や実装もまとめてみました。

まず、iOS の Push 通知は大きく分けて以下3種類あると思います。

  • 通常の Push 通知
  • サイレント Push 通知
  • リッチ Push 通知

通常の Push 通知

シンプルにタイトルとメッセージのみを表示するような通知です。アプリ側の実装としては受信してからの処理などは特に実装する必要がないのがメリットですが、受信した Push 通知のコンテンツのままタイトルやメッセージが表示されるので柔軟性は低いのがデメリットです。

スクリーンショット

ペイロードサンプル

{
    "aps":{
        "alert":{
            "title":"タイトル",
            "body":"メッセージ"
        },
        "sound":"default",
        "badge": 8
    }
}

サイレント Push 通知

通知の表示をしないで、受信したイベントのみ発生する通知です。Push 通知の受信時に何か特定の処理をさせたいといった時に活用できます。例えば、通常の Push 通知ではタイトルやメッセージは送られてきたまま表示されてしまいますが、Push 通知のペイロード(Push 通知に付与するデータ)を用いてカスタマイズしたタイトルやメッセージをローカル通知(通常の Push 通知を受信した時に表示されるものをアプリ側から呼び出して表示させること)で表示することが可能です。デメリットとしてはアプリ側で多少なり実装や設定が必要なことと送信側で Push 通知のペイロードに "content-available":1 が必須になったりと、ちょっと手間が増えます。また OS バージョンによっては正しく動作しない挙動(以下)もあるようなので注意が必要です。

https://qiita.com/MYamate_jp/items/d93618f6396fb00036c8
https://qiita.com/tonionagauzzi/items/64dd9d57f123a24a4e14

スクリーンショット

以下はペイロードから受け取った新たなタイトルやボディをローカル通知で表示した例。

ペイロードサンプル

data の部分は JSON フォーマットであれば、どのように定義しても問題ないです。

{
    "aps":{
        "badge": 8,
        "content-available" : 1
    },
    "data":{
         "newTitle":"Newタイトル",
         "newBody":"Newメッセージ"
    }
}

リッチ Push 通知

通常の Push 通知では文字のみが表示できましたが、iOS 10 で追加されたリッチ Push 通知では文字に加え、画像や動画、音声なども追加して表示させることができます。Push 通知の表示をカスタマイズすることが可能な反面、アプリの実装も必要になります。また、アプリ本体の Provisioning Profile の他に Notificaton Service Extension 用で Provisioning Profile が必要になります。さらに送信側も Push 通知のペイロードに "mutable-content":1 が必須になったりと、上記二つに比べて手間や設定の手順が増えます。

スクリーンショット

ペイロードサンプル

data の部分は JSON フォーマットであれば、どのように定義しても問題ないです。

{
    "aps": {
        "alert": {
            "title":"Title",
            "body":"Message"
        },
        "sound":"default",
        "badge": 8,
        "mutable-content" : 1
    },
    "data": {
        "newTitle":"Newタイトル",
        "newBody":"Newメッセージ",
        "imageUrl": "https://placehold.jp/150x150.png"
    }
}

アプリ側の設定・実装

Push 通知を受け取るためのアプリ側での設定や実装を確認しましょう。(Provisioning Profile や p12, p8 などの証明書関係の作成・設定手順等は省略します。)

共通実装

Push 通知を受け取るために上記3種類で共通の設定は Xcode の Capability から Push Notifications を追加する必要があります。

共通な実装は「通知の許可要求」と「デバイストークンの取得」になります。まず、「通知の許可要求」は requestAuthorization で要求し、「デバイストークンの取得」は registerForRemoteNotifications で APNs(Apple Push Notification service) へ登録を行い、登録に成功した場合に application(_:didRegisterForRemoteNotificationsWithDeviceToken:) のデリゲートが呼ばれるので、こちらから「デバイストークンの取得」を行います。なお、登録に失敗した場合は application(_:didFailToRegisterForRemoteNotificationsWithError:) のデリゲートが呼ばれます。取得したデバイストークンは Push を送信するサーバ側へ API などを経由して渡すのが一般的かと思います。

// プッシュ通知の許可を要求
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (granted, error) in
    if let error = error {
        print("プッシュ通知許可要求エラー : \(error.localizedDescription)")
        return
    }
    if !granted {
        print("プッシュ通知が拒否されました。")
        return
    }
    DispatchQueue.main.async {
        // APNs への登録
        UIApplication.shared.registerForRemoteNotifications()
    }
}
// APNs 登録成功時に呼ばれる
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
    print("デバイストークン : \(token)")
}

// APNs 登録失敗時に呼ばれる
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("APNs 登録に失敗しました : \(error.localizedDescription)")
}

また、Push 通知はアプリがフォアグラウンド(Active)の状態では表示されないです。フォアグラウンドの状態でも Push 通知の表示をさせるためには userNotificationCenter(_:willPresent:withCompletionHandler:) の実装が必要になります。

UNUserNotificationCenter.current().delegate = self

// フォアグラウンドで通知を受け取るために必要
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert, .sound])
}

通常の Push 通知の実装

通常 Push 通知を受け取るために必要なアプリ側の実装は共通の実装で終えているので、特に追加で行うことはありません。

サイレント Push 通知の実装

まず必要な設定が Xcode の Capability から Background Modes を追加し、Background Modes の中の Remote notifications にチェックを入れます。

その次に application(_:didReceiveRemoteNotification:fetchCompletionHandler:) を実装し、Push 通知を受信したことをアプリに通知されるようにします。プッシュ通知を送信する側はペイロードに "Content-available":1 が必須になります。以下の例はサイレント Push 通知を受信した際にペイロードから表示する文言を取得して、ローカル通知として表示する例になります。

// Push通知を受信した時(サイレントプッシュ)
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    guard let data = userInfo["data"] as? [String: Any],
            let newTitle = data["newTitle"] as? String,
            let newBody = data["newBody"] as? String else {
        completionHandler(.noData)
        return
    }
    // ローカル通知で表示するタイトルとメッセージを変更する
    showLocalNotification(identifier: "SilentPush", title: newTitle, body: newBody)
    completionHandler(.newData)
}

リッチ Push 通知の実装

まず、Xcode の File -> New から Target を選択し、Notification Service Extension を追加します。なお、アプリ本体とは別で Notification Service Extension 用の Provisioning Profile が必要になります。また、プッシュ通知を送信する側はペイロードに "mutable-content":1 が必須になります。

Target を追加すると UNNotificationServiceExtension を継承したクラスが生成されます。didReceive(_:withContentHandler:) は Push 通知を受信した際に呼ばれ、ペイロードなどから必要データを取得し、通知の内容を変更させることができます。 serviceExtensionTimeWillExpiredidReceive(_:withContentHandler:) 内で行う処理が30秒以内に終えられなかった場合に呼ばれます。以下の例は Push 通知のペイロードからタイトルやボディ、画像の URL を受け取り、それらを表示する例です。

class NotificationService: UNNotificationServiceExtension {

    private var contentHandler: ((UNNotificationContent) -> Void)?
    private var bestAttemptContent: UNMutableNotificationContent?

    // プッシュ通知を受信した時に呼ばれる
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        guard let bestAttemptContent = bestAttemptContent else {
            contentHandler(request.content)
            return
        }
        guard let data = request.content.userInfo["data"] as? [String: Any],
              let imageUrl = URL(string: data["imageUrl"] as? String ?? "") ,
              let newTitle = data["newTitle"] as? String,
              let newBody = data["newBody"] as? String else {
            contentHandler(bestAttemptContent)
            return
        }
        
        // タイトルとボディを書き換え
        bestAttemptContent.title = newTitle
        bestAttemptContent.body = newBody
        
        let downloadTask = URLSession.shared.downloadTask(with: imageUrl) { (url, _, _) in
            guard let url = url else {
                contentHandler(bestAttemptContent)
                return
            }
            // tempに保存
            let fileName = imageUrl.lastPathComponent
            let path = URL(fileURLWithPath: NSTemporaryDirectory().appending(fileName))
            
            do {
                try FileManager.default.moveItem(at: url, to: path)
                // 保存先のURLをプッシュ通知の表示領域に伝える
                let attachment = try UNNotificationAttachment(identifier: fileName, url: path, options: nil)
                bestAttemptContent.attachments = [attachment]
                contentHandler(bestAttemptContent)
            } catch {
                contentHandler(bestAttemptContent)
            }
        }
        downloadTask.resume()
    }
    
    // タイムアウト時に呼ばれる
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler,
           let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

サンプルコード

https://gist.github.com/TatsukiIshijima/d0e468950d19f70346048838eb14e4f9

補足

開発中で Push を送信するサーバ側が準備できていない時などは手元で Push 通知が受信できるか試したいかと思います。FCM(Firebase Cloud Messaging)などでも Push を送信することは可能なのですが、最近だとペイロードのカスタマイズなどができないようなので、Curl コマンドで APNs Provider API を実行してカスタムしたペイロード付きの Push を送信することができます。以下に参考になる送信スクリプトのリンクを載せます。

APNs Provider API での Push 送信スクリプトリンク集

https://qiita.com/yimajo/items/58565070d39acb4d5e71
https://qiita.com/dolfalf/items/2b65c77d11c4e8dbdd9a
https://qiita.com/KenNagami/items/f57b5a69eec091c50d4f

pem ファイル書き出し

スクリプトの中には pem ファイルが必要なものもあるので、書き出し方法も載せておきます。

openssl pkcs12 -clcerts -nodes -out [書き出し後のpemファイル名].pem -in [p12ファイル名].p12

Discussion