プッシュ通知を究める！その②〜リッチ通知（メディア付き）の実装の仕方〜


iOS10からプッシュ通知に画像や動画などを添付したり、カスタムUIを表示したりする **リッチ通知** が送れるようになりました。今回は **『メディア付きのプッシュ通知』** について説明します。

# メディア付きプッシュ通知（Media Attachments）

## メディア付きプッシュ通知とは

画像（GIFアニメ含む）や動画、音声をプッシュ通知に表示することができます。

受信したプッシュ通知のバナーを下スワイプしたり、ロック画面や通知センターの通知を **3Dタッチ** or左スワイプするとプッシュ通知を開くと下記のように表示/再生することができます。

GIFアニメを添付した際の見え方は下記のようになります（通知の下に出てくる「見てみる」「あとで」のボタンはアクション実行型プッシュ通知という別の仕組みです。また別途説明します）。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ステマ <a href="https://t.co/z77UiUBAP3">pic.twitter.com/z77UiUBAP3</a></p>&mdash; hideyuki nanashima (@jollyjoester) <a href="https://twitter.com/jollyjoester/status/820665417983139840">2017年1月15日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

他のメディアもそれぞれ下記のように見えます。

|画像|音楽|動画|
|---|---|---|
|![IMG_9130.PNG](https://qiita-image-store.s3.amazonaws.com/0/8868/68407f75-ed40-7e9f-0b24-ce6114df8f47.png "IMG_9130.PNG")|![IMG_9131.PNG](https://qiita-image-store.s3.amazonaws.com/0/8868/3af71d79-655f-08cb-f468-159154d9252f.png "IMG_9131.PNG")|![IMG_9132.PNG](https://qiita-image-store.s3.amazonaws.com/0/8868/6af6c8fb-550f-6683-a614-e111065882f4.png "IMG_9132.PNG")|

## メディア付きプッシュ通知の仕組み

メディア付きのプッシュ通知は`Notification Service Extension`というExtensionを実装することで実現することができます。

ざっくりとした仕組みは下記です。

1. 表示したいメディアをWeb上に配置してURLを取得する（リッチ通知の表示にはURLが必要）
2. プッシュ通知のペイロードに表示したいメディアのURLを含めて送信する
3. プッシュ通知を受信すると通知のバナーを表示する前に`Notification Service Extension`が起動されるので、ペイロード中のURLを用いてメディアをダウンロード＆tmp領域に保存する
4. メディアが保存が完了したらメディアと共に通知が表示される

## メディア付きプッシュ通知の実装

通常のプッシュ通知の準備ができてない方はまず[プッシュ通知を究める！その①〜普通のプッシュ通知の実装の仕方〜](http://qiita.com/jollyjoester/items/63991657f0d631383c9b)をご覧ください。

### Notification Service Extensionの作成

XcodeのメニューのFile -> New -> `Target...`をクリックします。

![Screen_Shot_2017-01-15_at_2_42_42_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/b7fe0628-2801-98ba-d60d-7e44d1834a24.png "Screen_Shot_2017-01-15_at_2_42_42_PM.png")

iOS -> Notification Service Extension -> `Next`をクリックします。

![Screen_Shot_2017-01-15_at_2_42_55_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/13ebb8b2-2a8a-9e34-4aa0-6dcd4260b1ad.png "Screen_Shot_2017-![Screen_Shot_2017-01-15_at_2_43_21_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/b16df79e-7eb7-8e93-9c6b-fd43cea47e52.png "Screen_Shot_2017-01-15_at_2_43_21_PM.png")


任意のProduct Nameを入力して`Finish`をクリックします。

![Screen_Shot_2017-01-15_at_2_43_21_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/b16df79e-7eb7-8e93-9c6b-fd43cea47e52.png "Screen_Shot_2017-01-15_at_2_43_21_PM.png")

下記のようなダイアログが出てきたら`Activate`をクリックします。

![Screen_Shot_2017-01-15_at_2_43_34_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/2624443a-2fae-2306-58d2-792e856fe0fe.png "Screen_Shot_2017-01-15_at_2_43_34_PM.png")

`Notification Service Extension`のフォルダができます。メディア付きプッシュ通知に必要な処理はこの時作成される`NotificationService.swift`の`didReceive`メソッド内に実装していきます。

![Screen_Shot_2017-01-15_at_2_44_30_PM.png](https://qiita-image-store.s3.amazonaws.com/0/8868/c8a2fc06-1c86-7db9-2223-eb2e35715300.png "Screen_Shot_2017-01-15_at_2_44_30_PM.png")

### Notification Service Extensionの実装

具体的な実装イメージは下記となります。

```NotificationService.swift
import UserNotifications

class NotificationService: UNNotificationServiceExtension {
    
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    
    // リッチ通知のプッシュ通知を受け取るとdidReceiveが呼ばれる
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let notificationData = request.content.userInfo["notification-data"] as? [String: String] {
            if let urlString = notificationData["attachment-url"], let fileURL = URL(string: urlString) {
                URLSession.shared.downloadTask(with: fileURL) { (location, response, error) in
                    if let location = location {
                        // メディアファイルをダウンロードしてtmpに保存
                        let tmpFile = "file://".appending(NSTemporaryDirectory()).appending(fileURL.lastPathComponent)
                        let tmpUrl = URL(string: tmpFile)!
                        try? FileManager.default.moveItem(at: location, to: tmpUrl)
                        
                        if let attachment = try? UNNotificationAttachment(identifier: "hoge", url: tmpUrl, options: nil) {
                            // メディアをtmp
                            self.bestAttemptContent?.attachments = [attachment]
                        } else {
                            // メディアの添付に失敗
                            self.bestAttemptContent?.body = "メディア添付に失敗した時の通知の文言"
                        }
                    }
                    contentHandler(self.bestAttemptContent!)
                    }.resume()
            } else {
                // URLの取得に失敗したときの処理
                contentHandler(self.bestAttemptContent!)
            }
        } else {
            // JSONペイロードからメディアのデータが取得できなかったときの処理
            contentHandler(self.bestAttemptContent!)
        }
    }
    
    // didReceiveの処理が30秒を超えると呼ばれる。
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            bestAttemptContent.body = "メディアのダウンロードがTimeoutになったときの通知の文言"
            contentHandler(bestAttemptContent)
        }
    }
}
```

### エラー処理

メディアのダウンロードなどに失敗したときなどはメディアが表示できません。メディアありきの文言になっていたりするとおかしなプッシュ通知になってしまいますので文言だけの通知に適した内容に差し替えたりすることもできます。

またメディアのダウンロードには制限時間（約30秒）があり、制限時間を超えると同じくメディアが表示されません。その場合は`serviceExtensionTimeWillExpire`メソッドが呼ばれるのでその中で同じく文言だけの通知に適した内容に差し替えるなどすると良いでしょう。（`contentHandler()`が30秒呼ばれないと`serviceExtensionTimeWillExpire`が呼ばれるようです。`contentHandler()`を実装し忘れたりすると`serviceExtensionTimeWillExpire`がもれなく呼ばれることになります。）

### プッシュ通知のペイロード

上記実装は下記のペイロードを送信することを前提とした実装です。`aps`内の`"mutable-content": 1`はリッチ通知を送る際に必須のものです。メディアのURLを送っている"notification-data"以下は任意の形式です。アプリ側の実装と合わせて自由に設計してください。

```JSON
{
  "aps": {
    "alert": "メディア付きリッチ通知だよ",
    "mutable-content": 1
  },
  "notification-data": {
    "attachment-url": "メディアのURL"
  }
}
```

（余談：メディアのダウンロードの際もATSの制約を受けます。httpsでアクセスできる場所にメディアを置いておくと良いでしょう（私はAWS S3に配置して公開してます。またATSの例外設定などをする場合はExtension内にある'Info.plist'に記述します。）

### 添付したメディア（画像/動画/音声）の判別

基本的にはtmp領域に保存した際の **URLの拡張子** でiOSが自動的に判断して出し分けてくれます。なので添付したメディアファイルの種類を正しく表した拡張子がついたURLを用いることをお勧めします。拡張子がないURLを使わざるをえない場合でも後述するファイルタイプのヒントを設定するオプションなどを使えば正しく表示することができます。

### サポートされてるメディアファイルの種類とサイズ

下記のページの **Supported File Type** に記載されています。けっこうでかいサイズまでサポートされていますが、ダウンロードの時間切れになると表示ができないので小さめのファイルを意識すると良いかもしれません（通信の速さにもよりますが2MBくらいまでは安定して表示された感触でした。）

https://developer.apple.com/reference/usernotifications/unnotificationattachment

### オプションなど

`UNNotificationAttachment`の初期化の際にオプションを設定することもできます。オプションは現在のところ下記の４つがあります。この中だとGIFアニメや動画の何秒目の部分をサムネイルにするか設定できる`UNNotificationAttachmentOptionsThumbnailTimeKey`が嬉しいですね！

|オプション名|内容|
|---|---|
| UNNotificationAttachmentOptionsTypeHintKey | メディアファイルの種類についてのヒント。UTI(Uniform Type Identifier)形式で指定する。拡張子なしのURLを使うときなどに使う。 |
| UNNotificationAttachmentOptionsThumbnailClippingRectKey | 画像や動画添付の場合、プッシュ通知に表示されるサムネイルの表示領域を設定する。|
| UNNotificationAttachmentOptionsThumbnailTimeKey | GIFアニメや動画添付の場合に何秒目の部分をサムネイルとして表示するかを設定する。 |
| UNNotificationAttachmentOptionsThumbnailHiddenKey | サムネイルを非表示にするか否かの設定。trueを設定すると非表示になる。|

ちなみにサムネイルとは下記の部分です。

![IMG_9133.png](https://qiita-image-store.s3.amazonaws.com/0/8868/26eef9e7-44cb-9a7b-6fd8-56b77705ec38.png "IMG_9133.png")


オプションを設定する場合は、必要なオプションを`[AnyHashable: Any]`のDictionaryに入れて`UNNotificationAttachment`を初期化する際に`options`に渡します。

```NotificationService.swift

let options: [AnyHashable: Any]  = [
	UNNotificationAttachmentOptionsTypeHintKey : kUTTypeJPEG,
	UNNotificationAttachmentOptionsThumbnailClippingRectKey : CGRect(x: 0, y: 0, width: 1, height: 1).dictionaryRepresentation,
	UNNotificationAttachmentOptionsThumbnailTimeKey:2,
	UNNotificationAttachmentOptionsThumbnailHiddenKey : true
] as [String : Any]
                        
if let attachment = try? UNNotificationAttachment(identifier: "hoge", url: tmpUrl, options: options)
{ ...
```

以上、何か間違い、質問等あればコメントで教えてください！
次はリッチ通知（カスタムUI付き）について書く予定です。


# 参考文献

https://developer.apple.com/reference/usernotifications/unnotificationserviceextension
https://developer.apple.com/reference/usernotifications/unnotificationattachment