【Firebase Remote Config】強制アップデート作ってみた

## 0. 初めに
先日、iOSアプリケーションの**強制アップデート**の実装をしました。
方法は色々あるなと考えたのですが、今回はGoogleさんが提供しているFirebase Remote Configを使用する方法が簡単だったので備忘録的にまとめました。

参考にしていただければ幸いです。


## 目次
1. [Firebaseのセットアップ](#1-Firebaseのセットアップ)
2. [Remote Configを設定する](#2-RemoteConfigを設定する)
3. [アプリとFirebaseを繋ぐ](#3-アプリとFirebaseを繋ぐ)
4. [強制アップデートのコードを実装する](#4-強制アップデートのコード実装を実装する)
5. [最後に](#5-最後に)


# 手順

Firebaseは、Googleのモバイルプラットフォームです。
Firebaseには[無料で使える便利な機能](https://firebase.google.com/pricing?hl=ja)が多く、ドキュメントも充実しており、読みやすいのでおすすめです。
今回はその中の、Remote Config機能を使用していきます。
## 1. Firebaseで新規プロジェクトを作成する

まずは[Firebaseの公式サイト](https://firebase.google.com/?hl=ja)へ行き、右上の"コンソールへ移動"します。

<img width="600" alt="スクリーンショット 2023-05-31 8.28.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/94337ef2-4f01-48f3-0e41-34b9c03b2388.png">


次に、Firebase プロジェクトから"プロジェクトを追加"し、作成手順へ進みます。
<img width="600" alt="スクリーンショット 2023-05-31 8.31.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/602cdeb7-d68f-ede1-0816-265a5b12435e.png">

手順通りに進めていきます。
<img width="600" alt="スクリーンショット 2023-05-31 8.37.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/4848877e-f3a9-ecb0-a509-0ce316f05f4d.png">

手順(2/3)のGoogleアナリティクスは有効にすることをおすすめします！
プロジェクト内で、AnalyticsやCrashlytics等を使用する際に必要となるからです。
<img width="600" alt="スクリーンショット 2023-05-31 8.38.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/55c40edb-e788-97fb-7cca-0e856904e85a.png">

手順(3/3)で"Default Account for Firebase"をセレクトし、プロジェクトを作成！
少し作成までに時間がかかることもありますが、これで完了です。（簡単だぜ）
<img width="600" alt="スクリーンショット 2023-05-31 8.38.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/19fcb3db-b99b-a379-2b5a-25fa0f2630ce.png">

以下のようなプロジェクト概要画面が表示されます。
<img width="600" alt="スクリーンショット 2023-05-31 8.43.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/fe017fa6-38e0-e4a0-4ebb-c02e8b3ca5ce.png">

## 2. Remote Configを設定する
Remote Configでは、任意の名前(パラメータ名)をつけた値(value)を登録できます。
Firebaseとアプリを繋ぎ、その値をアプリ側で取得することで様々な機能を実装できます！

今回実装する強制アップデートでは、事前にRemote Configに最新のアプリバージョンの値を登録しておき、Remote Configのバージョンと、ローカルのアプリのバージョンを比較することでアプリ上に強制アップデートの通知を出すように実装していきます。

それでは、プロジェクト概要画面の左から "構築 > Remote Config" を選んでいきましょう！
<img width="150" alt="スクリーンショット 2023-05-31 8.46.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/8edf08a4-bc9d-13f9-7d7b-73f0225daa2c.png">

以下の"構成を作成"へ進みます！ここでパラメータ名等を作ります。
<img width="600" alt="スクリーンショット 2023-05-31 9.02.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/d77cacaa-7fa7-61e9-2104-7b05a45f8521.png">

右側からパラメータの作成画面が出ますので、名前(パラメータ名)と値(value)、説明(Description)を入れます。
今回は最新のバージョンを登録したいので、名前(current_version)、値(2.0)、説明(現在ストアに公開中のアプリバージョン)を記入し、保存しましょう！
これでRemote Configの設定は完了です！
<img width="400" alt="スクリーンショット 2023-05-31 9.05.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/64b38346-2cf9-318d-8cbd-3c3509f24d11.png">


<details><summary>構成を公開のアラートについて</summary>

:::note warn
Remote Configの構成は公開しないとアプリ側で取得はできません。
なので以下のように警告が出ていますが、後で公開するので一旦スルーします。
<img width="600" alt="スクリーンショット 2023-05-31 9.09.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/f2a7b83b-cde3-256d-8733-1a1ac4c74fa0.png">
:::
</details>

## 3. アプリとFirebaseを繋ぐ
では次に、アプリとFirebaseを繋ぎます。
先ほど作成したFirebaseのプロジェクトにアプリを登録すると言った方がいいかもしれません。

まずは、適当なアプリを用意しましょう！
新規作成でもOKです。
<img width="600" alt="スクリーンショット 2023-05-31 16.11.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/af581b9e-a75b-4b1e-7224-cc68d937d878.png">

次に、"①アプリ > ②TARGET > ③Sining & Capability" から "④Bundle Identifier" をコピーします。
<img width="800" alt="スクリーンショット 2023-05-31 16.23.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/40dfc66c-c496-47d3-b263-fbb555e51fd5.png">

Firebaseの"プロジェクト概要"に戻り、"iOS+"をクリックします。
<img width="600" alt="スクリーンショット 2023-05-31 16.28.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/09ad05a6-8a10-c88b-c0f3-6e5406ece554.png">

先ほどコピーしたBundle Idcentifierを記入し、
以下、手順に従います。
<img width="400" alt="スクリーンショット 2023-05-31 16.30.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/1b8a9323-6285-3226-c138-d59fb3f1eed0.png">

手順②で、Info.plistをダウンロードして、アプリに入れましょう。
<img width="400" alt="スクリーンショット 2023-05-31 16.30.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/f9bc8b68-faae-e911-87d3-6171fa6923c6.png">
以下のように、自分のアプリのinfo.plistの下あたりにインポートすれば問題ありません。
<img width="600" alt="スクリーンショット 2023-05-31 16.34.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/8b7548cc-6a77-cd3d-d470-b4d56c6bfed0.png">

③手順にて、PackageをInstallします。
<img width="600" alt="スクリーンショット 2023-05-31 16.39.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/dc16b717-a58b-b01f-bb6b-41e95a795504.png">

以下のように、URLを入れ、"Add Package"をクリック
<img width="600" alt="スクリーンショット 2023-05-31 16.39.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/f888356c-0559-6a9e-6dd1-0cd8d8004c4c.png">

Packageが表示されるので、"FirebaseAnalytics"　と"FirebaseRemoteConfig"を追加し、
"Add Package"をクリック
<img width="600" alt="スクリーンショット 2023-05-31 16.42.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/0158e8fd-63e4-ea95-7e4e-32b9819386ed.png">

手順④の通りに、アプリのAppDelegate.swiftへ移動し、以下を記載

```diff_swift
    import UIKit
    import FirebaseCore //ここ追加だよ！
    
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
      var window: UIWindow?

      func application(_ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
          [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()　 //ここ追加だよ！

        return true
      }
    }
```

最後に、手順⑤を完了させれば連携は完了！！！


## 4. 強制アップデートのコード実装を実装する
 
あとはコードを書くだけです。
まずは全文書きます。


```diff_swift:AppDelegate.swift
import UIKit
import FirebaseCore

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        // Configure Firebase
       FirebaseApp.configure()
         
         //バージョン確認を実行
       VersionChecker.sharedChecker.setApplicationDidBecomeActive()
        
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}
```

```diff_swift:VersionChecker.swift
import UIKit
import Firebase
import FirebaseRemoteConfig
import FirebaseRemoteConfigSwift

class VersionChecker {
    private let remoteConfig = RemoteConfig.remoteConfig()
    
    static let sharedChecker = VersionChecker()
    
    public func setApplicationDidBecomeActive() {
        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
    }
    
    @objc private func applicationDidBecomeActive() {
        fetchRemoteConfigAndCheckVersion()
    }
    
    private func fetchRemoteConfigAndCheckVersion() {
        remoteConfig.fetch(withExpirationDuration: 0) { (status, error) in
            guard error == nil else {
                print("error in fetching. value: \(error)")
                return
            }
            self.remoteConfig.fetchAndActivate { _, _ in
                if self.checkVersion() {
                    self.showUpdateAlertIfNeeded()
                }
            }
        }
    }
    
    private func checkVersion() -> Bool {
        let currentVersion = remoteConfig.configValue(forKey: "current_version").stringValue ?? ""
        let localVersionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
        return currentVersion != localVersionString
    }
    
    private func showUpdateAlertIfNeeded() {
        guard let rootViewController = getTopViewController() else { return }
        
        let alertController = UIAlertController(title: "アップデートが必要です", message: "新しいバージョンがApp Storeにあります。アップデートしてください。", preferredStyle: .alert)

        let updateAction = UIAlertAction(title: "アップデート", style: .default) { _ in
            guard let url = URL(string: "https://www.apple.com/jp/app-store/"),
                  UIApplication.shared.canOpenURL(url) else { return }
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }

        let laterAction = UIAlertAction(title: "あとで", style: .cancel, handler: nil)
        
        alertController.addAction(updateAction)
        alertController.addAction(laterAction)
        rootViewController.present(alertController, animated: true, completion: nil)
    }
    
    private func getTopViewController(_ viewController: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? {
        if let navigationController = viewController as? UINavigationController {
            return getTopViewController(navigationController.visibleViewController)
        } else if let tabBarController = viewController as? UITabBarController, let selected = tabBarController.selectedViewController {
            return getTopViewController(selected)
        } else if let presented = viewController?.presentedViewController {
            return getTopViewController(presented)
        } else {
            return viewController
        }
    }
}
```


各部分の説明：

1. `Firebase`, `FirebaseRemoteConfig`, `FirebaseRemoteConfigSwift` で必要なフレームワークをインポートします。
2. `class VersionChecker`: バージョンのチェックと必要ならばアップデートのアラートを表示するためのクラスを定義します。
3. `private let remoteConfig = RemoteConfig.remoteConfig()`: Firebase Remote Configのインスタンスをプライベートプロパティとして保持します。
4. `static let sharedChecker = VersionChecker()`: VersionCheckerのシングルトンインスタンスを作成します。
5. `public func setApplicationDidBecomeActive()`: UIApplicationがアクティブになった時に呼ばれる関数を設定します。
6. `@objc private func applicationDidBecomeActive()`: UIApplicationがアクティブになった時にRemote Configを取得し、バージョンのチェックを行います。
7. `private func fetchRemoteConfigAndCheckVersion()`: Remote Configを取得し、バージョンのチェックを行う関数です。
8. `private func checkVersion() -> Bool`: Firebaseから取得したバージョンとローカルのアプリのバージョンを比較し、一致しなければtrueを返します。
9. `private func showUpdateAlertIfNeeded()`: バージョンが一致しない場合にアップデートを促すアラートを表示します。
10. `private func getTopViewController(...) -> UIViewController?`: 表示中の最上位のViewControllerを取得します。

今回の場合、アプリを開いた際にすぐに強制アップデートを起動させたいのでAppDelegateに`VersionChecker()`を記載しています。
また、開いた画面の最上位のViewControllerを取得することで、他の画面と被ることを防いでいます。
難しく見えますが、コードのブロックごとを見れば優しい内容です。


最後に、"Firebase RemoteConfig"へ戻り、変更を公開を実施します。
<img width="600" alt="スクリーンショット 2023-05-31 17.15.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/c0ede17d-55c4-e77e-6538-119dab170068.png">


公開後、アプリを起動すると、以下のAlertが出れば完成です！！！
<img width="400" alt="Simulator Screenshot - iPhone 14 Pro - 2023-05-31 at 17.28.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2516597/f28486c6-d857-c7c5-9d2a-a9277a58dd0f.png">

:::note warn
ローカルアプリのバージョンと、Firebase RemoteConfig上(構成のパラメーター値)のバージョンが同じな場合、アラートは出ません！
:::

## 5. 最後に

いかがでしたでしょうか？
RemoteConfigはこのような使い方だけでなく、他の使い方もあります。
今回はバージョンアップのみにフォーカスしました。

まだまだこの強制アップデートの課題として、バックエンド側の運用としては、RemoteConfigの値を管理していかなくてはいけない欠点はあります。ここら辺自動的に管理できれば楽なのですが・・・・（勉強不足）

少しでもみなさんの学習の一助になれば幸いです。
ではでは〜


参考：[RemoteConfigドキュメント](https://firebase.google.com/docs/remote-config?hl=ja&authuser=0)