Firebase Remote Configで強制アップデート

FirebaseのRemote Configを使用して強制アップデートを実装したので記事にします。

# 強制アップデートとは
アプリの新しいバージョンをリリースした時に、ユーザに必ずアップデートをして欲しい場合があります。
例えば、以下のような場合です。

- 既存の機能に大幅な変更が加わった場合
- 致命的なバグを修正した場合


この時、ユーザがアプリを立ち上げた際、ユーザにアップデートを促しアップデートを完了させるまでアプリの使用を事実上不可能にする処理が強制アップデートです。

アプリの使用を不可能にする処理としては、以下のようなアラートを出し、アプリストアへの遷移以外できなくすることなどが挙げられます。（ストアからアップデートせずに戻ってきても再度表示される）

![スクリーンショット 2022-01-12 20.12.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/72fa482a-180c-9e5c-f1ee-d85ad176d3ef.png)

また、強制アップデートは最初に公開した時に入れとかないと強制アップデートさせたくてもさせることができない場面が出てくるので、できるだけ最初のバージョンで入れておきたい機能です。

# Firebase Remote Configとは
Firebase Remote Configはアプリの動作や外見を変更するパラメータをFirebaseに持たせることができるクラウドサービスです。
アプリが通信処理を行いこのパラメータにアクセスすることにより、アプリ自身を変更することなく動作や外見を変更することができます。

今回の強制アップデートの場合、アプリの最新のバージョン情報などをFirebase Remote Configに設定しておきます。

# 実装
ここからは実際にどのような手順で実装するのか見ていきます。
## Firebase側

まずFirebaseのコンソールから左下、「エンゲージメント」の中にある「Remote Config」を選びます。

![スクリーンショット 2022-01-12 20.45.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/83d1c4e9-c012-074d-fcde-04ba77f08fc4.png)


初めての場合はRemote Configのトップページが表示されます。「構成を作成」を押すと最初のパラメータを入力することができるので、バージョン情報を入力して保存しましょう。
また、それぞれ項目は以下の通りです。

- パラメータ名
    - アプリ側でパラメータにアクセスするためのキー
- データ型
    - パラメータのタイプ
- Description
    - パラメータの説明
- Default value
    - 設定するパラメータ

![スクリーンショット 2022-01-12 20.50.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/7c487283-3a8d-108c-235a-2431f506cfc7.png)

 
保存すると以下のような画面になります。

![スクリーンショット 2022-01-12 20.55.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/0b535602-c38d-f500-a612-4327018b9382.png)


この状態ではまだ変更が適用されていません。右上の「変更を公開」ボタンを押すと変更が適用されて、設定したパラメータをアプリで使用できるようになります。
また、新たにパラメータを追加することも可能です。
私の場合は、`current_version`の他に`require_force_update`（強制アップデートさせるかどうか）と`app_store_url`（app storeのURL）も追加しています。

最後に「変更を公開」ボタンを押してFirebase側の設定は完了です。

![スクリーンショット 2022-01-12 21.00.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/b10cd6b0-bb83-f1dc-8a12-a097d3aef95e.png)

## アプリ側

次はアプリ側の実装についてです。
まず全体のコードを載せてしまいます。

```swift:RemoteConfigProvider.swift
import FirebaseRemoteConfig

/// Firebaseのコンソールで設定したパラメータ名に対応
enum ConfigKey: String {
    case currentVersion = "current_version"
    case forceUpdateRequired = "require_force_update"
    case storeUrl = "app_store_url"

    static func makeDefaults() -> [String: Any] {
        [
            currentVersion.rawValue: "",
            forceUpdateRequired.rawValue: false,
            storeUrl.rawValue: ""
        ]
    }
}

final class FirebaseRemoteConfigProvider {
    private let remoteConfig = RemoteConfig.remoteConfig()

    /// パラメータを取ってくる
    func fetchConfig(completion: (() -> Void)? = nil) {

        // 取得するパラメータのデフォルトを設定
        remoteConfig.setDefaults(ConfigKey.makeDefaults() as? [String: NSObject])
        
        // FetchとActivateを一括で行う
        remoteConfig.fetchAndActivate(completionHandler: { status, error in
            switch status {
            case .successUsingPreFetchedData, .successFetchedFromRemote:
                completion?()
            case .error:
                if let error = error {
                    print(error.localizedDescription)
                }
            @unknown default: fatalError()
            }
        })
    }

    /// RemoteConfigから取ってきたパラメータを取得
    func getConfig(key: ConfigKey) -> RemoteConfigValue {
        remoteConfig.configValue(forKey: key.rawValue)
    }
}
```

```swift:ForceUpdateManager.swift
import Combine
import FirebaseRemoteConfig

final class UpdateCheckManager {

    static let shared = UpdateCheckManager()
    private let remoteConfigProvider = FirebaseRemoteConfigProvider()
    private var cancellable: AnyCancellable?

    private init() {}

    func setup() {
        observeApplicationDidBecomeActive()
    }

    /// アプリがActiveになった際にアップデートチェック
    private func observeApplicationDidBecomeActive() {
        cancellable = NotificationCenter.Publisher(center: .default, name: UIApplication.didBecomeActiveNotification, object: nil)
            .sink(receiveValue: { [weak self] _ in
                FirebaseRemoteConfigProvider().fetchConfig(completion: {
                    self?.forceUpdateIfNeeded()
                })
            })
    }

    /// requireForceUpdateがtrueかつ現在のバージョンが最新のバージョンと異なる場合に強制アップデート
    private func forceUpdateIfNeeded() {
        let localVersionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
        let requireForceUpdate = remoteConfigProvider.getConfig(key: .forceUpdateRequired).boolValue

        if requireForceUpdate, 
           let currentVersionString = remoteConfigProvider.getConfig(key: .currentVersion).stringValue,
           localVersionString != currentVersionString 
        {
            guard let storeUrlString = remoteConfigProvider.getConfig(key: .storeUrl).stringValue,
                  let storeUrl = URL(string: storeUrlString)
            else {
                return
            }
            // 強制アップデートのアラートを出す
        }
    }
}

```
```swift:AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UpdateCheckManager.shared.setup()
        return true
    }
    ...
}
```
各クラスの説明は以下の通りです。

|クラス|機能|
|---|---|
|UpdateCheckmanager|強制アップデートに関する処理を担当|
|FirebaseRemoteConfigProvider|Firebase Remote Configとの通信とパラメータ取得|
|ConfigKey|Firebase Remote Configのパラメータ名を定義|

コードを見ていただくとわかりますが、`UpdateCheckManager`で`didBecomeActive`を監視しています。アプリがアクティブになった際は、`FirebaseRemoteConfigProvider`にFirebaseのコンソールで設定したパラメータを取ってきてもらいます。

また今回は、「アプリがアクティブになった際に強制アップデートのアラートを出す」という方針を取っております。これにより、ストアに移動してアップデートせずに戻ってきた場合でも再度アラートを出すことができます。

# まとめ
Firebase Remote Configを使用した強制アップデートの実装についてまとめました。
強制アップデートの実装については、他にも強制アップデート用のライブラリを利用する等の方法があるようなので、作成するアプリに適した方法を使用してください。


# 参考文献
[Firebase Remote Configを使ってみる](https://firebase.google.com/docs/remote-config/get-started?hl=ja&platform=ios)
[Implementing Force Update Feature using Firebase Remote Config in iOS](https://medium.com/codechai/implementing-force-update-feature-using-firebase-remote-config-in-ios-52beaabebab5)