おいしい健康 開発者ブログ

株式会社おいしい健康で働くエンジニア・デザイナーが社内の様子をお伝えします。

iOSのPush通知受信イベントをログしようとして陥った問題

はじめに

こんにちは、おいしい健康で主にiOSアプリの開発を行っている、むろやと申します。 個人的に初のテックブログ記事かつ、2022年初のおいしい健康開発者ブログ記事となります🎉 今回は、iOSアプリで

  • 画像を表示するPush通知をクライアント側で受け取れるようにするためにやったこと

  • 受信のイベントを取得しようとして陥った問題

について書いてみます🧑‍💻

おいしい健康のPush通知の仕組み

本題に入る前に、簡単に、おいしい健康でどうやってPush通知を送っているのか、その仕組みを簡単にご案内します💡 簡単なシーケンス図がこちらです👇 Screen Shot 2022-02-16 at 13.20.57.png (63.5 kB)

シーケンス図補足

おいしい健康では、Firebase Cloud Messaging(以降FCM)を使って、Push通知の送信を行っています。FCMについて詳しく知りたい方はこちらを御覧ください。

  • FCMのPayload作成用バッチ

    • 例えば、毎日おすすめのレシピを送りたい、と思った場合には、毎日定期実行されるバッチJobを作成し、その中でおすすめのレシピを持ってきてPayloadの形に加工してFCMのPayload用DBに追加していきます
  • FCMのPayload用DB

    • 各レコードがStateというカラムで送信済みかどうかを判定できるようになっています
  • FCM送信用バッチ

    • Stateをチェックして、未送信のものだけを抽出し、FCMに対して送信します

こんな感じでバックエンドが動くことで、日々Push通知が送信されています。 それでは本題に入っていきます。

画像を表示するPush通知をクライアント側で受け取れるようにするためにやったこと

まずは画像の表示についてですが、iOSでプッシュ通知に画像を表示したいとなった場合には、FCMのPayloadに画像を含む必要があるだけでなく、クライアント側で追加の実装をする必要があります。

具体的には、UNNotificationServiceExtensionというクラスのサブクラスを独自に作成し、画像表示のロジックを実装していくことが求められます。

おいしい健康のiOSアプリでは、下記のようなカスタムクラスを作成しています👇

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
            contentHandler(request.content)
            return
        }

        let userInfo = request.content.userInfo as NSDictionary
        let fcmOptions = userInfo["fcm_options"] as? [String: String]

        guard let fcmOptions = fcmOptions,
              let url = URL(string: fcmOptions["image"] ?? "") else {
            contentHandler(content)
            return
        }

        let task = URLSession.shared.dataTask(with: url, completionHandler: { data, _, error in
            let fileName = url.lastPathComponent
            let writePath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)

            do {
                try data?.write(to: writePath)
                let attachment = try UNNotificationAttachment(identifier: fileName, url: writePath, options: nil)
                content.attachments = [attachment]
                contentHandler(content)
            } catch {
                print("error: \(error)")
                contentHandler(content)
            }
        })
        task.resume()
    }

    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

やっていることとしては、

  1. プッシュ通知のPayloadから添付画像のURLを取り出す

  2. 添付画像ファイルをダウンロードする

  3. 2の保存先のURLをOSに提供する

というような感じです。

こちらは色々と参考になる記事も見つけられたので、あまり苦戦しないところかなあと思っております。 引っかかる部分があるとすれば、上記クラスはApp Extensionとしてプロジェクトに追加していく必要があるため、XcodeGenを導入しているようなプロジェクトにおいては、project.ymlを良い感じに変更する必要があります。

メインのtarget配下で下記のように依存関係を追加し、

dependencies:
- target: NotificationService
  codeSign: false
  embed: true

また、別途NotificationServiceExtension用のターゲットを下記のように追加する必要があります。

NotificationService:
    type: app-extension
    platform: iOS
    sources:
      - path: NotificationService
        excludes:
          - "Info.plist"
    ~以下省略~

上記はNotificationServiceのハマりどころというよりは、App Extensionを追加する際のハマりどころといえます。

受信のイベントを取得しようとして陥った問題

さて、今回本当にお伝えしたいのはこちらです。 新規でPush通知を追加する時に、どのPush通知がどれだけ効果があるのか、ということを分析したくなるというのはよくあることだと思います。 FCMのダッシュボードで確認できるじゃないかという声が聞こえてきそうですが、Android端末だと各Push通知の開封率など詳細に確認ができる一方で、iOS端末宛の通知についてはほぼすべてのデータが確認できないという仕様になっています😢(本ブログ公開時点) なので、自前で分析できるような環境を準備する必要があります。

開封率を見るためには、

  • 通知を受信したタイミング

  • 通知を開いた(タップした)タイミング

でそれぞれイベントのログを行う必要があります。

通知を開いた(タップした)タイミングに関しては、例えば、userNotificationCenter(_:didReceive:withCompletionHandler:) などを実装していけば簡単にログを行うことができます。

ところが、受信のタイミングで発火するようなAPIは特に用意がされていません。

そこでいくつか方法を考えました。例えば、

  1. 👆で実装したNotificationService内で、Firebase SDKなどを使ったイベントのログを行う

  2. 👆で実装したNotificationServiceから、メインのターゲットに対してイベントを通知し、そこからイベントのログを行う

などの方法を考えましたが、1についてはFirebase SDKAppExtensionをサポートしていない(本ブログ公開時点)という問題があり断念せざるを得ませんでした。 2についても、NotificationServiceは別ターゲットである以上、メインターゲットのアプリとはプロセスが別になってしまっており、アプリが起動していない状態ではイベント通知がうまくいかないという問題がありました。

ですので現状、結論としては、Push通知の受信をしたかどうかというイベントはとれないことがわかりました🤮これは非常に不便で、FCMには是非iOS端末の詳細のデータについてもカバーしてほしいと願うばかりです🥺

おいしい健康では現在は、アプリ受信可能なユーザー(プッシュ通知の許可をしているユーザー)数を見つつ、開封のイベント数をみて、擬似的に開封率を監視するという手段をとっています。

さいごに

以上、Push通知について主に書かせていただきました。 これからも継続してFCMやFirebase SDKAppleの通知関連のSDKの動向を見ながら、より効果的にPush通知を運用していけるような体制を作っていきたいと思います💪