【Firebase】Remote Config を使ってiOSアプリにメンテナンス中表示を組み込む手順

今回は、サーバーメンテナンス中にアプリを触って欲しく無い時などに、Firebase が提供している機能 Remote Config を使ってアプリの動作を管理する例を紹介したいと思います。

※Firebase のプロジェクト作成、SDKインストールや初期設定のプログラムなどについては下記の記事の見出し(1)〜(2)の内容をご参考ください

https://www.yururiwork.net/%e3%80%90swiftui%e3%80%91firebase-authentication-%e3%81%ae-ios-%e5%b0%8e%e5%85%a5%e6%89%8b%e9%a0%86%e3%80%90%e2%91%a0%e5%9f%ba%e6%9c%ac%e8%a8%ad%e5%ae%9a%e7%b7%a8%e3%80%91/

Remote Config を使ってiOSアプリにメンテナンス中表示を組み込む手順

(1) コンソール画面でパラメータを作成する

Firebase のコンソール画面左側のメニューを下の方にスクロールすると「Remote Config」のメニューがありますのでクリックすると以下のような画面が表示されます。

Remote Config は JSON 形式でデータを管理します。上記赤枠の「パラメータの追加」欄でパラメータキー(ファイル名)と JSON のデータを設定します。

パラメータキーは任意の名称で結構です。今回は「server_maintenance_config」としてみました。

続いて、デフォルト値の入力欄の右側の中括弧({})のマークをクリックすると JSON エディタが開きます。

入力中に JSON 形式として正しいかエラーチェックをしてくれるので便利ですね。

今回は、メンテナンス中かどうかの真偽値と、ダイアログに表示するタイトル及びメッセージを含めました。

保存をクリックして閉じましょう。

すると公開待ち状態の画面となりますので、変更を公開をクリックします。

変更は直ちにユーザーのアプリに影響を与えますので、既にアプリ側のプログラムが配布されている時に変更を加える場合は誤りが無いかしっかりと確認してから公開するよう気をつけましょう。

今回は手順紹介ですので構わず公開してしまいます。

(2) CocoaPods で Remote Config の SDK をインストール

Firebase Authentication などと同様に Remote Config の SDK をインストールする必要があります。

Podfile を開き以下の行を追加します。

pod 'Firebase/RemoteConfig'

保存したら pod install でインストールしましょう。

% pod install
Analyzing dependencies
Downloading dependencies
Installing Firebase 6.32.2
Installing FirebaseABTesting (4.2.0)
Installing FirebaseRemoteConfig (4.9.1)
Generating Pods project
Integrating client project
Pod installation complete! There are 6 dependencies from the Podfile and 24 total pods installed.

(3) Swift コードから JSON データを取得する例

コードを書く準備が整ったので、早速実装例を紹介します。

折角なので今後の拡張も考えてちゃんとクラス化してみました。

RemoteConfigParameterKey 列挙体

enum RemoteConfigParameterKey: String, CaseIterable {
    case serverMaintenance = "server_maintenance_config"
}

先程、コンソール画面で定義したパラメータキーをアプリ側で定義します。今後パラメータキーを作成したらこちらに case を追加して行きます。

ServerMaintenanceConfig 構造体

struct ServerMaintenanceConfig: Codable {
    let isUnderMaintenance: Bool
    let title: String
    let message: String
}

Remote Config で取得したJSON形式のデータをデコードする際のデータ型を定義します。後述しますが、JSONDecoder でデコードするため予め Codable プロトコルに準拠しておきます。

RemoteConfigClientProtocol プロトコル

protocol RemoteConfigClientProtocol {
    func fetchServerMaintenanceConfig(succeeded: @escaping (ServerMaintenanceConfig) -> Void, failed: @escaping (String) -> Void)
}

必須ではありませんが、一応スタブ環境など、ビルドターゲットによって取得データを変えられるようにプロトコルを切っておきました。

RemoteConfigClient クラス

先程のプロトコルを継承し、シングルトンクラスとして実装しました。

import Foundation
import Firebase

class RemoteConfigClient: RemoteConfigClientProtocol {
    
    static let shared = RemoteConfigClient()
    
    private var remoteConfig: RemoteConfig
    
    private init() {
        remoteConfig = RemoteConfig.remoteConfig()
        let settings = RemoteConfigSettings()
        settings.fetchTimeout = 30
        #if DEBUG
        settings.minimumFetchInterval = 0
        #endif
        remoteConfig.configSettings = settings
    }
    
    func fetchServerMaintenanceConfig(succeeded: @escaping (ServerMaintenanceConfig) -> Void, failed: @escaping (String) -> Void) {
        remoteConfig.fetchAndActivate(completionHandler: { [weak self] status, error in
            
            guard let `self` = self else { return }
            
            switch status {
            case .successFetchedFromRemote, .successUsingPreFetchedData:
                
                guard
                    let jsonString = self.remoteConfig[RemoteConfigParameterKey.serverMaintenance.rawValue].stringValue,
                    let jsonData = jsonString.data(using: .utf8) else {
                    return
                }
                
                do {
                    let config = try JSONDecoder().decode(ServerMaintenanceConfig.self, from: jsonData)
                    succeeded(config)
                } catch let error as NSError {
                    let errorMessage = error.localizedDescription
                    failed(errorMessage)
                }
                
            case .error:
                if let error = error {
                    let errorMessage = error.localizedDescription
                    failed(errorMessage)
                }
            default:
                return
            }
        })
    }
}

少し詳しく見ていきます。

フェッチ間隔の設定
#if DEBUG
settings.minimumFetchInterval = 0
#endif

Remote Config は一度フェッチするとキャッシュされるため、コンソールで更新をしてもしばらく再フェッチされません(デフォルトでは12時間)。Firebase 側で設定されている割り当て制限に達することを避けるためのようです。

イニシャライザーでデバッグビルド時は RemoteConfigSettings.minimumFetchInterval に 0 を設定しています。こうすることで、開発中は常に更新をリアルタイムに反映することができます。

くれぐれも本番ビルド時には 0 が設定されることが無いよう気をつけましょう

フェッチの実行
remoteConfig.fetchAndActivate(completionHandler: { [weak self] status, error in
    switch status {
    case .successFetchedFromRemote, .successUsingPreFetchedData:
        ・・・
    case .error:
        ・・・
}

fetchAndActivate で全ての Remote Config の設定がフェッチされます。単に fetch というメソッドもありますが、こちらは取得だけ行い任意のタイミングで有効化したい時に使用します(今回は触れません)。

completionHandlerstatusRemoteConfigFetchStatus 型)で成功か失敗か確認します。

  • successFetchedFromRemote:フェッチに成功した場合
  • successUsingPreFetchedData:キャッシュされたデータを取得した場合(一応成功)
  • error:何らかの原因でフェッチが失敗した場合
フェッチされたデータのデコード
let jsonString = self.remoteConfig[RemoteConfigParameterKey.serverMaintenance.rawValue].stringValue
let jsonData = jsonString.data(using: .utf8)
let config = try JSONDecoder().decode(ServerMaintenanceConfig.self, from: jsonData)

フェッチが成功、またはキャッシュが取得できたら remoteConfig インスタンスからパラメータキーを指定して JSON 形式の文字列を取得します。

更にData型に変換した後、JSONDecoder().decode を使用して ServerMaintenanceConfig 型に変換しています。

SwiftUI での使用例

最後に SwiftUI での使用例を紹介して終わります。UIKit の場合は viewDidLoad などで実行すると良いと思います。

struct SplashView: View {

    @State var isShowingAlert = false
    @State var alertTitle = ""
    @State var alertMessage = ""

    var body: some View {
        VStack {
            Text("Splash")
                .alert(isPresented: $isShowingAlert) {
                    Alert(
                        title: Text(alertTitle),
                        message: Text(alertMessage),
                        dismissButton: .none
                    )
                }
        }
        .onAppear() {
            RemoteConfigClient.shared.fetchServerMaintenanceConfig(
                succeeded: { [weak self] config in
                    if config.isUnderMaintenance {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
                            guard let `self` = self else { return }
                                self.alertTitle = config.title
                                self.alertMessage = config.message
                                self.isShowingAlert.toggle()
                        }
                    }
                }, failed: { [weak self] errorMessage in
                    print(errorMessage)
                }
            )
        }
    }
}

ちなみに、サーバーメンテナンス中の場合アプリを直ぐに強制終了させたくなりますが、単に exit(1) でアプリを終了してしまうと審査で引っかかる可能性が高いので、例えば「OKを押すとアプリが終了します」などで強制終了することをユーザーにわかるようにしておくことを推奨します。

以上

1件のコメント

メンテナンスモードだと即時反映が必要だと思いますが、これだと12時間後にメンテモードとなる場合があるかと思いますが、いかがでしょうか?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です