【Swift】アプリを再インストールした際にFCMのリモート通知が届かなくなった

## 環境
- Xcode Version 12.3 (12C33)
- iOS 13 以上
- Firebase/Messaging (7.1.0)


## 初期実装

`AppDelegate` では以下のような初期化処理を行っていた

```swift:AppDelegate.swift

import FirebaseCore
import FirebaseMessaging

import UIKit


// MARK: - AppDelegate

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        self.setupFirebase(application: application)
        
        let window = UIWindow()
        self.window = window

        return true
    }

    private func setupFirebase(application: UIApplication) {
        
        FirebaseApp.configure()
        
        Messaging.messaging().delegate = self

        application.registerForRemoteNotifications()
    }
```

FCMトークンが必要な場面で以下のように呼び出しを行っていた

```swift
import FirebaseMessaging

// 中略

let fcmToken = Messaging.messaging().fcmToken
```

## 問題発覚

- アプリをアンインストール後再インストールするとリモート通知が届かなくなった
- サーバーのログを確認すると `"error":"No information found about this instance id."` とエラーが出ていた

## 原因
- 再インストール後 `Messaging.messaging().fcmToken` を利用するとアンインストール前の FCM トークンが返却されていた

## 対応
- **`Messaging.messaging().token(completion:)` を一度呼び出すことで `completion` 呼び出し後には有効なトークンが取得できるようになる**
- `setupFirebase(application:)` を以下の通り修正

```swift:AppDelegate.swift
    private func setupFirebase(application: UIApplication) {
        
        FirebaseApp.configure()
        
        Messaging.messaging().delegate = self
        
        // iOSは起動時に意図的にFCMトークンを取得する処理を追加することで「アプリが再インストール」された場合も対応できる
        Messaging.messaging().token { (_, error: Error?) in
            
            guard error == nil else { return }
            
            application.registerForRemoteNotifications()
        }
    }
```

# 検証

## 検証１: アプリ再インストール時の挙動

`MessagingDelegate` の `func messaging(_ messaging:didReceiveRegistrationToken fcmToken:)` を以下の通り実装

```swift:AppDelegate.swift
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        
        print("*** fcmToken: \(fcmToken ?? "")")
        print("*** fcmToken: \(Messaging.messaging().fcmToken ?? "")")
    }
```

- `didReceiveRegistrationToken`の`fcmToken`と `Messaging.messaging().fcmToken`が一致することは確認している

1. 通知が届く状態のアプリをデバッグ実行し`didReceiveRegistrationToken` デリゲートメソッドに届いたFCMトークンを確認
2. FCMトークンが有効か curl コマンドで確認
3. デバッグ実行を中止しアプリをアンインストール
4. アプリをデバッグ実行（再インストール）し`didReceiveRegistrationToken` デリゲートメソッドに届いたFCMトークンを確認
5. FCMトークンが有効か確認
6. １分程度放置し`didReceiveRegistrationToken`デリゲートメソッドが再び発火するか確認
7. PUSH通知が届くか確認
8. アプリを再起動（デバッグ実行）
9. `didReceiveRegistrationToken` デリゲートメソッドに届いたFCMトークンを確認
10. FCMトークンが有効か確認
11. １分程度放置し`didReceiveRegistrationToken`デリゲートメソッドが再び発火するか確認
12. PUSH通知が届くか確認

### 結果

1. トークンAを得る
2. トークンAは curl 有効であることがわかる
3. 実行可
4. トークンAを得る
5. トークンAは curl 無効であることがわかる（ `"error":"No information found about this instance id."` ）
6. 発火しない（トークンAは更新されない）
7. 届かない
8. 実行可
9. トークンBを得る
10. トークンBは curl コマンドより有効であることがわかる
11. 発火しない（トークンBは更新されない）
12. 届く

## 検証2: トークン取得メソッドを変更しエラーハンドリングを試みる

`MessagingDelegate` の `func messaging(_ messaging:didReceiveRegistrationToken fcmToken:)` を以下の通り実装

```swift:AppDelegate.swift
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        
        print("*** fcmToken: \(fcmToken ?? "")")
        print("*** fcmToken: \(Messaging.messaging().fcmToken ?? "")")
        
        Messaging.messaging().token { (token: String?, error: Error?) in
            
            print("*** Messaging.messaging().token")
            print("*** fcmToken: \(token ?? "")")
            print("*** fcmTokenError: \(error?.localizedDescription ?? "nothing")")
        }
    }

```

以下の手順を試みる

1. 通知が届く状態のアプリをデバッグ実行し`didReceiveRegistrationToken` ログを確認
2. FCMトークンが有効か curl コマンドで確認する
3. デバッグ実行を中止しアプリをアンインストール
4. アプリを再インストール（デバッグ実行）し`didReceiveRegistrationToken` 内部のログを確認


### 結果

1. 以下の通りトークンAを得る

```
*** fcmToken: [トークンA]
*** fcmToken: [トークンA]
*** Messaging.messaging().token
*** fcmToken: [トークンA]
*** fcmTokenError: nothing
```

2. トークンA はcurl コマンドより有効であることがわかる
3. 実行可能
4. 以下のログを得る

```
*** fcmToken: [トークンA]
*** fcmToken: [トークンA]
*** fcmToken: [トークンB]
*** fcmToken: [トークンB]
*** Messaging.messaging().token
*** fcmToken: [トークンB]
*** fcmTokenError: nothing
*** Messaging.messaging().token
*** fcmToken: [トークンB]
*** fcmTokenError: nothing
```

curl コマンドによる追加検証で以下が判明している

- 上記ログ2行目時点ではトークンAは有効
- 上記ログ3行目時点ではトークンAは無効
- トークンBは有効


## 検証結果まとめ

1. アプリを再インストールした際に`didReceiveRegistrationToken`でアンインストール前に利用していたトークンが返却される
2. `Messaging.messaging().fcmToken` では `didReveiveRegistrationToken` で得られるトークンと同じものを取得する
3. `didReceiveRegistrationToken`発火後何らかの原因でアンインストール前に利用していたトークンが無効化される
4. 無効化された後アプリを再起動することで正常に通信可能なトークンを`didReceiveRegistrationToken`、`Messaging.messaging().fcmToken`で利用可能となる
5. `Messaging.messaging().token(completion:)`メソッドを用いることで再インストール後にも有効なFCMトークンを得ることができる
6. その際`Messaging.messaging().token(completion:)`メソッドにはエラーが入ってこない
7. `Messaging.messaging().token(completion:)`メソッドを呼び出すと`didReveiveRegistrationToken`が発火し、内部で有効なFCMトークンを得ることができる
8. `Messaging.messaging().token(completion:)`メソッドを呼び出した後には`Messaging.messaging().fcmToken`プロパティで有効なFCMトークンを得ることができる