FirebaseのCloud Messagingを使ってサイレント通知を試してみた

#はじめに
仕事で社内用アプリの開発の際に、サイレント通知を使用したかったため、試しにFirebaseの[Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/?hl=ja)の機能を使用してみました。
通知を使わなくなりそうなので、とりあえず供養…

#開発環境
- Xcode 9.0
- Swift 3.2

#事前準備
基本的な設定は[iOSでのFirebase Cloud Messagingクライアントアプリの設定](https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ja)をご覧になっていただければいいかと思うのですが…
ソース的には[Firebase Messaging Quickstart(GitHub)](https://github.com/firebase/quickstart-ios/tree/master/messaging)のソースにあるものを漏れなく記載してあれば通知の受け取りは動作すると思います。

####1.CocoaPodのインストール
Podfileを以下のように変更して`pod install`を行なってください。

```:Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'NotificationTest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for NotificationTest
  pod 'Firebase/Core'
  pod 'Firebase/Messaging'

end
```
CocoaPodのインストール等についてわからない場合は、こちら[【Swift】CocoaPods導入手順](http://qiita.com/ShinokiRyosei/items/3090290cb72434852460)の記事等を参考にして導入してみてください。


#APNs認証キーのアップロード
APNs認証キーをアップロードします。事前にAPNs認証キーを作成しておいてください。
作成の際は[FCMでのAPNsの設定](https://firebase.google.com/docs/cloud-messaging/ios/certs?hl=ja)をご参照いただければ、基本的にはいいかとは思うのですが、Apple Developer Member Centerの画面操作の説明が少々古い内容ではないかと思うので、わからなかったら[別記事にまとめたもの](https://qiita.com/miyae/items/5567458c17f5ee5cf720)を参照してみてください。


①Firebaseコンソールの[Overview]の右側にある設定マークをクリックし、表示されたメニューから[プロジェクトの設定]を選択します。
![24.png](https://qiita-image-store.s3.amazonaws.com/0/195038/7d2d6c5c-699f-f890-b4f0-0a1a094a8348.png)



②[クラウドメッセージング]タブを選択します。
![25.png](https://qiita-image-store.s3.amazonaws.com/0/195038/298de6a1-590a-5c63-237d-542b6814c5c2.png)


画面下の[iOSアプリの設定]の[APNs認証キー]の[アップロード]をクリックします。
![26.png](https://qiita-image-store.s3.amazonaws.com/0/195038/3a4bbb11-b1a2-82ea-1935-6f0bd9de6be3.png)




③[APNs認証キーのアップロード]画面がポップアップ表示されるので、[参照]から、Apple Developer Member Centerで作成した[APNs認証キー]と、[キーID]を設定します。（[App ID Prefix]は自動で設定されるままで問題ないと思います）設定後、[アップロード]をクリックします。
![27.png](https://qiita-image-store.s3.amazonaws.com/0/195038/4a64c1ed-8d91-d3a0-1d91-adb2849d3500.png)



④処理終了後、ファイルが正常にアップロードされたことをコンソール画面で確認してください。
![28.png](https://qiita-image-store.s3.amazonaws.com/0/195038/612e279b-b944-a2c9-2af6-9b76250c73f6.png)


#通知を受け取る
基本的な設定は[iOS での Firebase Cloud Messaging クライアント アプリの設定](https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ja)とかぶる部分もありますが、設定が済んでいる部分については読み飛ばしてください。

####1.基本設定
以下はAppDelegate.swiftに設定を行います。
`UserNotifications`と`Firebase`をインポートしてください。

```swift:import
import UserNotifications
import Firebase
```

使用する端末がiOS10以降のOSを搭載している場合は、`MessagingDelegate`と`UNUserNotificationCenterDelegate`のデリゲートの設定が必要です。
ドキュメントに注意書きがしてありますが、UNUserNotificationCenterの設定はソースの記載があるのに、**MessagingDelegateの方は記載されていないのでご注意ください。**


```swift:application(didFinishLaunchingWithOptions)
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        //Firebaseの共有インスタンスの設定をします
        FirebaseApp.configure()

        // Cloud Messagingのデリゲートを設定します（iOS10以降）
        Messaging.messaging().delegate = self as MessagingDelegate

        // 通知の設定を行います（iOS10以前と以降を分けます
        if #available(iOS 10.0, *) {

            // For iOS 10 display notification (sent via APNS)
            UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
            
            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: {_, _ in })
        } else {
            let settings: UIUserNotificationSettings =
                UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }
        
        application.registerForRemoteNotifications()
        
        return true
    }
```

```swift:
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print("APNs token retrieved: \(deviceToken)")
    }

```


####2.サイレント通知を受け取った場合
サイレント通知を受け取った場合は以下のメソッドで通知を受け取ります。
デバッグした限り、アプリがフォアグラウンドの際もバックグラウンドで受け取った際もどちらも以下で受け取りました。

```swift:
    //サイレントで通知を受け取った場合
    func application(_ application: UIApplication,
                     didReceiveRemoteNotification userInfo: [AnyHashable : Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void){
        //通知を受け取った時の処理
        if let messageID = userInfo[gcmMessageIDKey] {
            print("Message ID: \(messageID)")
        }
        
        // Print full message.
        print(userInfo)
    }
```

####3.通知を受け取った場合
画面にポップアップ表示される通知を受け取った際に以下のメソッドが通知を受け取ります。
使用する端末がiOS10以降のOSを搭載している場合は、`UNUserNotificationCenterDelegate`の拡張をして通知を受け取ります。（iOS9以前は違うようですが、手元に9の端末がなかったため試していません）

```swift:
@available(iOS 10, *)
extension AppDelegate : UNUserNotificationCenterDelegate {
    
    //アプリ起動時に通知を受け取った時に通る
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        
        // messageIDを出力
        if let messageID = userInfo[gcmMessageIDKey] {
            print("Message ID: \(messageID)")
        }
        
        // 受信した全メッセージを出力
        print(userInfo)
        
        // 優先する通知オプションの変更を行う場合は設定する
        completionHandler([])
    }

    //受け取った通知を開いた時に通る
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        // Print message ID.
        if let messageID = userInfo[gcmMessageIDKey] {
            print("Message ID: \(messageID)")
        }
        
        // Print full message.
        print(userInfo)
        
        completionHandler()
    }
}
```



#通知を送る
通知送信の内容については、公式ドキュメントの[FCM メッセージについて](https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ja)を参照していただきたいのですが、サイレント通知を行う場合はメッセージのタイプが`データメッセージ`のデータを送信することになります。

今回、サイレント通知を送る対象が特定の単一端末だったため、FCM登録トークンを指定して送信したかったのですが…
[Cloud MessagingのGUIDS](https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ja)をご覧になっていただければわかると思うのですが、コンソールからの送信、トピックへの送信、端末グループへの送信、アップストリームメッセージの送信…
プッシュ通知もJSONも初心者の自分には何をどうしたらいいやら…？？？
よくわからないけど、JSONを送信するようにしてみるか…と、コーティングしたもののうまくいかない…
とりあえず、送信しようとしている内容が悪いのか受信側のコーディングが悪いのか切り分けするため、サイレント通知の受信動作を確認したかったため、こちら（[【iOS10】Firebaseでサイレント通知を行う](https://qiita.com/shipaaan/items/099f8e7aa37d5e2540da)）の記事を参考にさせていただいて、Postmanを使用してサイレント通知のJSONを送信してみました。
送信しているJSONの内容を書き換えつつ、テストテストテスト…
と、あれこれしてうまくいったのが以下。
JSONの変換とかライブラリ使ったほうがいいのだろうかなどは考えたのですけど、結局標準フレームワークのみでコーディングしました。

①GoogleService-Info.plistにKeyの追加
テスト時はベタでコードに書いてもいいかなと思うのですけど、一応Firebaseのサーバーキーをplistに保存しておきます。
サーバーキーはFirebaseの[設定]>[クラウドメッセージング]から確認してください。
![serverkey.png](https://qiita-image-store.s3.amazonaws.com/0/195038/22a43ccd-e22e-7950-0dea-dc9fe1ed2bab.png)

GoogleService-Info.plistには以下のように追加してください。
![plist.png](https://qiita-image-store.s3.amazonaws.com/0/195038/cd62f55c-a14d-86bc-be3d-039d518ce96f.png)

#####②FCMトークンの取得
単一の端末に通知を送信する場合、各端末のトークンの情報が必要になります。トークンは、以下のように記載して取得します。
`Firebase`をインポートしていれば取得可能ですが、トークンが生成されていない段階で取得しようとした場合はnilが返されることがあります。

```swift:トークンの取得（適宜追加して使用）
let token = Messaging.messaging().fcmToken
```
テストする場合は、自分のトークンを取得して設定すれば自身で取得して確認できるので良いですが、実際のに使用する際は他の端末に送信するでしょうし、その場合は他の端末のトークンが必要になるので、DB等に保存して取得するようにする必要があります。
私はアプリ作成の際はユーザのテーブルにトークンも保存して、トークンに変更があったタイミングで更新をかけるようにしていました。（何かしらのタイミングでトークンが変更されることがあるそうなのでご注意ください。）
トークンが変更されるタイミングの捕捉ですが、監視したい場合は以下のようにデリゲートを登録して、デリゲートメソッドを追加すると良いです。

```swift:AppDelegate.swift
extension AppDelegate : MessagingDelegate {
    //トークンの生成のモニタリング
    func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
        print("Firebase registration token: \(fcmToken)")
    }    
}
```

③送信するJSONデータ
以下のようにデータメッセージの形式に沿ったJSONを送信すると、ポップアップは表示されず、内部的にデータの受信が行われます。

```json:
{
    "to" : [送信先のFCM Tokenを設定],
    "priority" : "high",
    "data" : {
        "testData" : "TestJSON"
    },
    "content_available" : true
}
```
`to`：送信先の設定です。今回は送信先のFCMトークンを設定します。
`priority`：優先度の設定です。データメッセージはデフォルトではnomalになるのですが、その場合、スリープ状態の端末で通知が遅延することがあるとドキュメントに記載があったので、変更しました。
`content_available`：アプリのスリープ状態の解除をするためにはこれをtrueに設定する必要があります。
`data`：カスタムのKey-Valueペアを設定できます。必要なければ記載しなくても問題ありません。


④JSONデータをPOSTするコードを追加
以下のコードで実行できたので、以下のコードの動きを試したい場合は、ボタンのイベント等から呼び出すようにして、`送信先のFCMトークン`を引数として渡してやってください。

```swift:ViewController.swift
    func postDataMessage(idToken:String){
        //GoogleService-Info.plistへのパスを取得します
        let firebasePlist = "GoogleService-Info"
        guard let googlePlist = Bundle.main.path(forResource: firebasePlist, ofType: "plist")
            else{print("GoogleService-Info.plistの取得失敗"); return}

        //SERVER_KEYを取得します
        if let option = NSDictionary(contentsOfFile: googlePlist){
            guard let serverKey = option["SERVER_KEY"] as? String
                else{print("SERVER_KEYの取得失敗"); return}

            //通知送信用のURL設定
            let url = URL.init(string:"https://fcm.googleapis.com/fcm/send")

            //リクエストの設定
            var request = URLRequest(url: url!)
            request.httpMethod = "POST"
            //ヘッダーの設定（keyにサーバーキーを設定します）
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.addValue("key=\(serverKey)", forHTTPHeaderField: "Authorization")

            do{
                //送信するJSONデータの設定
                var sendData = Dictionary<String,Any>()
                //既定のパラメータに設定を行います
                sendData["to"] = idToken
                sendData["priority"] = "high"
                sendData["content_available"] = true
                
                //"data"以下のネスト用
                var dataDic = Dictionary<String,Any>()
                dataDic["testData"] = "TestJSON"
                sendData["data"] = dataDic
                
                //JSONデータの生成
                let sendJsonData = try JSONSerialization.data(withJSONObject: sendData, options: [])
                request.httpBody = sendJsonData
                
                //セッションの確立
                let config = URLSessionConfiguration.default
                let session = URLSession(configuration: config, delegate: nil, delegateQueue: .main)

                //JSONデータの送信
                let task = session.dataTask(with: request, completionHandler: {(data, response, error) -> Void in
                    if(error == nil){
                        if data == nil{
                            print("HTTP通信でエラー")
                            return
                        }else{
                            print("data : \(data!.description)")
                        }
                    }else{
                        print("Error : \(error.debugDescription)")
                    }
                })
                task.resume()
            }catch let error{
                print("try-catch Error : \(error.localizedDescription)")
            }
        }
    }
```

上記のPOSTで送信したデータメッセージをAppDelegateで受信した際の内容は以下になります。

```
[AnyHashable("testData"): TestJSON, AnyHashable("aps"): {

    "content-available" = 1;

}, AnyHashable("gcm.message_id"): 0:1509610714214997%8ae518be8ae518be]
```

#おわりに
最初にサイレント通知の処理がうまくいかなかったのは自分の知識の無さが原因としか言いようがないのですが、サイレント通知関連の話はネット検索してもなかなか見つからない気がします。（探すのが下手くそなだけかもしれませんが。）
あまり需要がないのでしょうか？
わかりませんが…私は使わないことになりそうな流れの今、備忘録のつもりでまとめましたが、これがもしどなたかの参考になれば幸いと思います。
個人的には、通知関連とJSON関連の勉強になったので、今後何かのタイミングで生かしていければと思います。
だいたい毎回言っているのですが…もし何かございましたらコメント等いただければ幸いです。