記事

iOS Notification Service Extension|Swiftで画像推播と表示統計を最適化

iOS 10以上対応のNotification Service Extensionで画像付きプッシュ通知の表示前処理を自動化。推播表示の統計取得も可能にし、ユーザー体験を向上させる具体的な実装方法を解説。

iOS Notification Service Extension|Swiftで画像推播と表示統計を最適化

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


iOS ≥ 10 Notification Service Extension の応用 (Swift)

画像プッシュ通知、プッシュ通知表示統計、プッシュ通知表示前処理

基礎的なプッシュ通知の構築や仕組みについてはネット上に多くの情報があるため、ここでは詳しく説明しません。本記事の主なポイントは、アプリで画像付きプッシュ通知をサポートし、新機能を活用してより正確な通知表示の統計を実現する方法です。

上図のように、Notification Service Extensionを使うと、アプリがプッシュ通知を受け取った後に通知の事前処理ができ、その後に通知内容を表示できます。

公式ドキュメントには、プッシュ通知の内容を処理する際、処理時間の制限は約30秒であると記載されています。30秒を超えてコールバックがない場合、プッシュ通知はそのまま実行され、ユーザーの端末に表示されます。

サポート状況

iOS ≥ 10.0

30秒で何ができる?

  • (目標1) プッシュ通知の内容にある画像リンクのフィールドから画像をダウンロードし、通知内容に添付する🏆

  • (目標2) プッシュ通知の表示有無を統計する🏆

  • プッシュ通知内容の修正、再構成

  • プッシュ通知内容の暗号化・復号(復号)表示

  • プッシュ通知を表示するか決められる? =>> 答え:できません

まず、バックエンドのプッシュ通知プログラムの Payload 部分

後端がプッシュ通知を送る際に、構造に "mutable-content":1 を追加する必要があります。これにより、システムは通知サービスエクステンションを実行します。

1
2
3
4
5
6
7
8
9
10
11
{
    "aps": {
        "alert": {
            "title": "新しい記事をおすすめします",
            "body": "今すぐ確認"
        },
        "mutable-content":1,
        "sound": "default",
        "badge": 0
    }
}

And… 最初のステップとして、プロジェクトに新しいターゲットを作成する

**Step 1.** Xcode -> ファイル -> 新規作成 -> ターゲット

Step 1. Xcode -> File -> New -> Target

**Step 2.** iOS -> Notification Service Extension -> Next

Step 2. iOS -> Notification Service Extension -> Next

**Step 3.** Product Name を入力 -> 完了

Step 3. 製品名を入力 → 完了

**Step 4.** Activateをクリック

Step 4. 「Activate」をクリックしてください

ステップ2:プッシュ通知の内容処理プログラムを書く

Product Name/NotificationService.swiftファイルを見つける

Product Name/NotificationService.swiftファイルを見つける

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UserNotifications

class NotificationService: UNNotificationServiceExtension {

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

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let bestAttemptContent = bestAttemptContent {
            // 通知内容をここで変更します…
            // プッシュ通知の内容をここで処理し、画像を読み込みます
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
            contentHandler(bestAttemptContent)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        // システムによって拡張機能が終了される直前に呼ばれます。
        // ここで修正した「最善の試み」の内容を配信する機会として使用してください。そうしないと元のプッシュペイロードが使用されます。
        // タイムアウトになるので、画像は無視してタイトルの内容だけ変更します
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

上記のコードでは、NotificationServiceに2つのインターフェースがあります。1つ目は didReceive で、通知が届いたときにこの関数が呼ばれます。処理が完了したら、contentHandler(bestAttemptContent) というコールバックメソッドを呼んでシステムに通知します。

時間が長すぎて CallBack メソッドが呼ばれない場合、2 番目の関数 serviceExtensionTimeWillExpire() がトリガーされます。タイムアウトとなり、基本的に手遅れなので、タイトルや内容を単純に変更するなどの後処理のみ行い、ネットワークからのデータ読み込みは行いません。

実践例

ここでは、Payload が以下のようになっていると仮定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "aps": {
        "alert": {
            "push_id":"2018001",
            "title": "新しい記事をおすすめします",
            "body": "すぐに確認する",
            "image": "https://d2uju15hmm6f78.cloudfront.net/image/2016/12/04/3113/2018/09/28/trim_153813426461775700_450x300.jpg"
        },
        "mutable-content":1,
        "sound": "default",
        "badge": 0
    }
}

「push_id」と「image」はどちらもカスタムフィールドで、push_idはプッシュ通知を識別しサーバーへ統計送信を行うために使います。imageはプッシュ通知に添付する画像のURLです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
    if let bestAttemptContent = bestAttemptContent {
        
        guard let info = request.content.userInfo["aps"] as? NSDictionary,let alert = info["alert"] as? Dictionary<String,String> else {
            contentHandler(bestAttemptContent)
            return
            // 通知内容の形式が予期したものと異なるため、処理しない
        }
        
        // 目標2:
        // サーバーに通知が表示されたことを返す
        if let push_id = alert["push_id"],let url = URL(string: "顯示統計API網址") {
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
            request.httpMethod = "POST"
            request.addValue(UserAgent, forHTTPHeaderField: "User-Agent")
            
            var httpBody = "push_id=\(push_id)"
            request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
            request.httpBody = httpBody.data(using: .utf8)
            
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                
            }
            DispatchQueue.global().async {
                task.resume()
                // 非同期処理なので気にしない
            }
        }
        
        // 目標1:
        guard let imageURLString = alert["image"],let imageURL = URL(string: imageURLString) else {
            contentHandler(bestAttemptContent)
            return
            // 画像が添付されていなければ特別な処理はしない
        }
        
        
        let dataTask = URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
            guard let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(imageURL.lastPathComponent) else {
                contentHandler(bestAttemptContent)
                return
            }
            guard (try? data?.write(to: fileURL)) != nil else {
                contentHandler(bestAttemptContent)
                return
            }
            
            guard let attachment = try? UNNotificationAttachment(identifier: "image", url: fileURL, options: nil) else {
                contentHandler(bestAttemptContent)
                return
            }
            // 画像のURLを読み込み、ダウンロードして端末に保存し、UNNotificationAttachmentを作成
            
            bestAttemptContent.categoryIdentifier = "image"
            bestAttemptContent.attachments = [attachment]
            // 通知に画像の添付ファイルを追加
            
            bestAttemptContent.body = (bestAttemptContent.body == "") ? ("すぐに確認") : (bestAttemptContent.body)
            // bodyが空の場合はデフォルトの内容「すぐに確認」を設定
            
            contentHandler(bestAttemptContent)
        }
        dataTask.resume()
    }
}

serviceExtensionTimeWillExpire の部分は特に処理していないので、省略します;重要なのは上記の didReceive のコードです。

プッシュ通知を受信した際、まずAPIを呼び出してバックエンドに通知を受け取り表示することを伝え、プッシュ通知の統計管理を容易にします。その後、画像が添付されている場合は画像の処理を行います。

In-App状態の場合:

同様にNotification Service ExtensionのdidReceiveがトリガーされ、その後AppDelegateの func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any ], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) メソッドが呼び出されます。

注釈:画像プッシュ通知についてはさらに…

Notification Content Extension を使って、プッシュ通知をタップしたときに表示する UIView をカスタマイズ(自作可能)し、タップ時の動作も設定する方法

参考はこちらの記事をご覧ください: iOS10プッシュ通知の応用(Notification Extension)

iOS 12以降、より多くのアクション処理をサポート: iOS 12 新通知機能:インタラクティブな通知の実装

Notification Content Extensionの部分では、画像付き通知を表示できるUIViewを一つだけ用意し、それ以上の工夫はしていません:

[結婚吧APP](https://itunes.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E4%B8%8D%E6%89%BE%E6%9C%80%E8%B2%B4-%E5%8F%AA%E6%89%BE%E6%9C%80%E5%B0%8D/id1356057329?ls=1&mt=8){:target="_blank"}

結婚吧APP

ご質問やご意見がありましたら、こちらからご連絡ください

Post は Medium から ZMediumToMarkdown によって変換されました。


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。

© ZhgChgLi. All rights reserved.
閲覧数: 802,415+, 最終更新日時: 2026-01-15 11:14:58 +08:00

本サイトは Chirpy テーマを使用し、Jekyll 上で構築されています。
Medium の記事は ZMediumToMarkdown により変換されています。