iOSでFirebase Cloud Messagingを使う

## 実行環境

* Xcode 8.3.1
* Swift 3.1
* Deployment Target 9.0
* Firebase/Core (3.15.0)
* Firebase/Messaging (3.15.0)

## はじめに

iOSでFirebase Cloud Messaging(FCM)を使う記事はいくつか紹介されていますが、本記事では次の方針で実装する方法を紹介します。

## 方針

* Method swizzlingを有効にする
* 通知の許可が得られていない状態での通知を受け取りを行わない

## Method swizzlingについて

Method swizzling自体はFirebase特有のものではなく、既存のメソッド実装を自前の実装に差し替えるためのプログラミングの手法です。  
FCMのライブラリではこのMethod swizzlingを使用しています。  
使用している箇所は[Firebaseのドキュメント](https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ja)に以下のように記載されています。  
なお、Method swizzlingは"メソッドの実装入れ替え"と翻訳されています。


>FCM API では、FCM 登録トークンに対する APNs トークンのマッピングと、ダウンストリーム メッセージのコールバック処理中の分析データの取得という 2 つの主要領域でメソッドの実装入れ替えを行います。 入れ替えを使用しない場合、アプリの Info.plist ファイルにフラグ FirebaseAppDelegateProxyEnabled を追加し、これを NO（ブール値）に設定することによって、入れ替えを無効にできます。このガイドの関連領域には、メソッドの実装入れ替えを有効にした場合としない場合の両方のコード例が記されています。

### Method swizzlingを有効にすべきか？

今回は無効にするメリットが見つからず、Method swzzlingを有効にした方が実装するコード量が減るため、有効にしました。  

## 通知の許可が得られていない状態での通知を受け取りについて

### そんな事は可能なのか？

FCMは通知の許可が得られていない状態で通知を受け取る機能を提供しています。  
この点に関して、ドキュメントで触れられているところを見つけることが出来ず、検証して気が付きました。
ただし、通知が許可されていないケースではAPNSは使えませんので、APNSを使った通知と機能の差異があります。  
例えば、アプリが起動されていないと通知を受け取ることが出来ません。  
検証した限りでは、通知を許可していない状態では次の振る舞いをしていました。

* アプリがフォアグラウンドにいるとき通知を受信できる。
* アプリがフォアグラウンドに遷移した時にアプリがバックグラウンドにいた時に通知された通知を受信できる。
  * ただし、フォアグラウンドに遷移した時に、すぐに呼ばれるわけではない。

### この機能を使わないことは可能か？

可能です。  
実装方法については後半で記載します。

### この機能とどのように向き合っていくべきか？

この機能を使ってしまうと、FCMから乗り換える際の足枷になってしまうため基本的には使わない方が良いと考えています。

## 実装

ここから実装方法について紹介していきます。  
この記事ではSwiftコードしか記載しませんのでライブラリの導入の仕方や、FCM管理画面の設定は次の記事などをご参照ください。  

* [Firebaseを利用してプッシュ通知を受け取る (iOS)](http://qiita.com/ysk_1031/items/e0fce5ec0c31c0ede0d6)

また、説明の都合上Firebaseに関係ない箇所も記載しています。

### 通知のパーミッション取得

Firebase関係ないですが、下記コードでパーミッションを要求します。  
`registerForRemoteNotifications()`を呼ぶことで、`AppDelegate`の`func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)`が呼ばれます。

```swift
protocol PushNotificationSettingsPresenter { }

extension PushNotificationSettingsPresenter {
    func presentPushNotificationSettings() {
        
        let application = UIApplication.shared
        
        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().requestAuthorization(
                options: [.badge, .sound, .alert],
                completionHandler: { (granted: Bool, error: Swift.Error?) in
                    if let _ = error { return }
                    if granted == false { return }
                    
                    application.registerForRemoteNotifications()
            })
        } else {
            let settings = UIUserNotificationSettings(
                types: [.alert, .badge, .sound],
                categories: nil)
            application.registerUserNotificationSettings(settings)
            application.registerForRemoteNotifications()
        }
     }
}
```

### Firebaseのトークン取得

Firebaseのトークンは`FIRInstanceID.instanceID().token()`で取得できます。  
ただ、発行前にこちらにアクセスするとnilが返ってきてしまう事があるらしく、オブザーバを使う方が確実だそうです。  
オブザーバに登録したselectorは起動後ほぼすぐに一度呼ばれるのと、`registerForRemoteNotifications()`を呼んだ後で呼ばれていました。  
なお、Firebaseのトークンはパーミッションの取得に関係なく発行されます。

```swift
    func addRefreshFcmTokenNotificationObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.fcmTokenRefreshNotification(_:)),
            name: .firInstanceIDTokenRefresh,
            object: nil)
    }
    
    func fcmTokenRefreshNotification(_ notification: Notification) {
        guard let refreshedFcmToken = FIRInstanceID.instanceID().token() else { return }
        
        // サーバにToken保存などの処理
    }
```

### APNSのトークン取得

Firebase関係ありませんが、記載しておきます。

```swift
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
        
        // サーバにToken保存などの処理
    }
```

### 通知の受信処理

今回は通知の許可を得られていない状態での通知の受信は行わないため`func applicationReceivedRemoteMessage(_ remoteMessage: FIRMessagingRemoteMessage)`では何も実装しません。  
何も実装しないことで、通知の許可を得られていない状態での通知の受信を使わないようにしています。
ただし、ドキュメントにiOS10以降では`FIRMessagingDelegate`を実装しないとMethod Swizzilingが有効にならないと記載があったため、Delegateの実装と設定のみ行っています。

```swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
            FIRMessaging.messaging().remoteMessageDelegate = self
        }
    }


    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> ()) {
        
        switch application.applicationState {
        case .inactive:
            // 通知からアプリを起動した時の処理

        case .active:
            // アプリ起動中に通知を受け取った時の処理
            
        case .background:
            ()
            
        }
        
    }


@available(iOS 10.0, *)
extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo: [AnyHashable: Any] = notification.request.content.userInfo
        // 通知からアプリを起動した時の処理
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo
        // アプリ起動中に通知を受け取った時の処理
    }
}

extension AppDelegate: FIRMessagingDelegate {
    func applicationReceivedRemoteMessage(_ remoteMessage: FIRMessagingRemoteMessage) {
        /*
         本メソッドは通知を許可していない場合に呼ばれる。
         本Delegateを実装しないとFCMのmethod swizzlingが無効化されるとドキュメントに記載されていたため、Delegateの実装のみ行う。
         なお、本メソッドを使う場合はFIRMessaging.messaging().connectを使い、FCMとコネクションをはる必要がある。
         */
    }
}
```

## 補足

今回、必要な実装は以上になりますが、検証した中で得られたナレッジがいくつかありますので記載しておきます。
ただ、アプリには取り込んでいない機能であり、あまり検証しきれていない部分もあると思います。ご了承下さい。

### 補足1: FIRMessaging.messaging().connect, disconnectの実装は必要か？

[Firebaseのドキュメント](https://firebase.google.com/docs/cloud-messaging/ios/receive?hl=ja)には実装が必要と記載されていますが、今回のように通知の許可を得られていない状態での通知受け取り機能を使わない場合は実装不要でした。

### 補足2: Firebaseのトピック

Firebaseの管理画面には通知のターゲットとしてトピックが用意されています。  
これの追加方法がわからなかったのですが、アプリでトピックをsubscribeする処理を実行すると管理画面に追加されるようです。  
以下のコードで`all`というトピックが追加されます。

```swift
FIRMessaging.messaging().subscribe(toTopic: "/topics/all")
```

## おわりに

まだ本番運用できていないので、気がつけていない点などあるかもしれませんが、検証を終えたので共有させて頂きました。