🪶

SwiftUIで、RemoteConfigを使ってみた

2024/07/31に公開

Firebase Remote Configを使う

Firebase Remote Configとは?

公式より引用

アプリのアップデートを公開しなくても、アプリの動作と外観を変更できます。コストはかからず、1 日あたりのアクティブ ユーザー数の制限もありません。

Firebase Remote Config は、ユーザーにアプリのアップデートをダウンロードしてもらわなくても、アプリの動作や外観を変更できるクラウド サービスです。Remote Config を使用して、アプリの動作や外観を制御するためのアプリ内デフォルト値を作成できます。その後、Firebase コンソールか Remote Config バックエンド API を使用して、すべてのアプリユーザーまたはユーザーベースの特定セグメントに対して、アプリ内デフォルト値をオーバーライドできます。アップデートを適用するタイミングはアプリ側で制御できます。アプリはアップデートの有無を頻繁にチェックし、パフォーマンスにほとんど影響をおよぼすことなくアップデートを適用できます。

iOSの解説はこちら

でもSwiftUIのやり方が書いてない???

海外動画を見たりして参考にしてましたね。
https://www.youtube.com/watch?v=CyYixjtxl9E

Firebaseの環境構築をして使ってみた💦

SPMを使って、firebase-ios-sdkを追加します。

https://github.com/firebase/firebase-ios-sdk

私の場合は、後でまた追加する設定しないと、importの箇所でエラー出ました???

Firebaseに、SwiftUIのバンドルIDを追加して、GoogleServiceinfo.plistをSwiftUIのプロジェクトに追加してください。

終わったら、RemoteConfigの設定をしていきます。

設定の手順はこんな感じです

SwiftUIは、プロジェクトを作ったときは、Version1.0なので、Firebase側には、1.1と設定すると、後でビルドしたときに、強制アップデートのアラートを表示できるはずです。

Firebase Remote Configには、以下の2つのキーを設定する必要があります:

  1. force_update_required
    • 型: Boolean
    • 説明: 強制アップデートが必要かどうかを示すフラグ
    • 例: true(強制アップデートが必要な場合)、false(強制アップデートが不要な場合)
  2. force_update_current_version
    • 型: String
    • 説明: 必要な最小バージョン
    • 例: "1.1"(バージョン1.1以上が必要な場合)

Firebase Remote Configのコンソールで、これらのキーと値を以下のように設定します:

  1. キー: force_update_required 値: true(強制アップデートを有効にする場合)
  2. キー: force_update_current_version 値: "1.1"(アプリの現在のバージョンが1.0なので、これより大きい値を設定)

これらの設定により:

  • force_update_requiredtrueの場合にのみ、バージョンチェックが行われます。
  • アプリの現在のバージョンがforce_update_current_versionで指定されたバージョンよりも低い場合、アップデートが必要と判断されます。

設定後、Remote Configの値を公開(Publish changes)することを忘れないでください。また、テスト時はRemote Configの最小フェッチ間隔を0に設定し、アプリを再起動するたびに新しい値をフェッチするようにすることをお勧めします。

このアプローチにより、アプリのバージョンとRemote Configの設定を柔軟に管理でき、必要に応じて強制アップデートを有効/無効にすることができます。




ソースコードには、FirebaseCoreとRemoteConfigの設定をすれば、成功していればアラートを表示する機能を実装できます。

今回は、処理をプロジェクトが作られたときからある2つのファイルでしか使ってないので、印をつけている箇所に書いて貰えば大丈夫です。

エントリポイントになるファイルには以下のように設定。

import SwiftUI
import FirebaseRemoteConfigInternal
import FirebaseCore

class RemoteConfigManager: ObservableObject {
    private var remoteConfig: RemoteConfig
    @Published var forceUpdateRequired = false
    
    init() {
        remoteConfig = RemoteConfig.remoteConfig()
        let settings = RemoteConfigSettings()
        settings.minimumFetchInterval = 0 // For testing, set to a higher value in production
        remoteConfig.configSettings = settings
        
        fetchRemoteConfig()
    }
    
    func fetchRemoteConfig() {
        remoteConfig.fetch { [weak self] status, error in
            if status == .success {
                self?.remoteConfig.activate { _, error in
                    self?.checkForceUpdate()
                }
            } else {
                print("Config not fetched")
                print("Error: \(error?.localizedDescription ?? "No error available.")")
            }
        }
    }
    
    private func checkForceUpdate() {
        let forceUpdate = remoteConfig.configValue(forKey: "force_update_required").boolValue
        let requiredVersion = remoteConfig.configValue(forKey: "force_update_current_version").stringValue ?? ""
        let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
        
        if forceUpdate && isVersionLower(current: currentVersion, required: requiredVersion) {
            DispatchQueue.main.async {
                self.forceUpdateRequired = true
            }
        }
    }
    
    private func isVersionLower(current: String, required: String) -> Bool {
        let currentParts = current.split(separator: ".").compactMap { Int($0) }
        let requiredParts = required.split(separator: ".").compactMap { Int($0) }
        
        for i in 0..<max(currentParts.count, requiredParts.count) {
            let currentPart = i < currentParts.count ? currentParts[i] : 0
            let requiredPart = i < requiredParts.count ? requiredParts[i] : 0
            
            if currentPart < requiredPart {
                return true
            } else if currentPart > requiredPart {
                return false
            }
        }
        
        return false
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    FirebaseApp.configure()

    return true
  }
}

@main
struct RemoteConfigTutorialApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject private var remoteConfigManager = RemoteConfigManager()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(remoteConfigManager)
        }
    }
}

ContentViewには、アラートを表示するコードを書きます。RemoteConfigのバージョンが、1.0より上の1.1~以降であれば、アプリをアップデートするのを提案してくるアラートが表示できるはずです。

リリースしているアプリだと、AppStoreへのリンクがあるはずですが、今回はローカルで動かすだけなのでつけてないです。

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var remoteConfigManager: RemoteConfigManager
    
    var body: some View {
        VStack {
            Text("Welcome to the app!")
        }
        .alert(isPresented: $remoteConfigManager.forceUpdateRequired) {
                    Alert(
                        title: Text("更新が必要です"),
                        message: Text("アプリの新しいバージョンが利用可能です。アプリを引き続き使用するには更新してください。"),
                        dismissButton: .default(Text("OK")) {
                            // テスト用なので、ここでは何もアクションを起こしません
                            print("更新アラートが解除されました")
                        }
                    )
                }
    }
}

ビルドするとこのような表示が出てくれば成功です!

使われているコードの説明

Firebase SDKを追加したら、Remote Config シングルトン オブジェクトを作成します。

remoteConfig = RemoteConfig.remoteConfig()
let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
remoteConfig.configSettings = settings

公式の解説によると
このオブジェクトは、アプリ内デフォルト パラメータ値の保存、更新されたパラメータ値の Remote Config バックエンドからのフェッチ、フェッチされた値がアプリで使用できるようになるタイミングの制御に使用されます。

開発時には、比較的短い最小フェッチ間隔を設定することをおすすめします。詳細については、スロットル処理をご覧ください。

アプリ内デフォルト パラメータ値を設定する:
アプリ内デフォルト パラメータ値を Remote Config オブジェクトに設定すると、Remote Config バックエンドに接続する前にアプリを意図したとおりに動作させることができます。また、バックエンド側に値が設定されていない場合は、これらのデフォルト値を使用できます。

curlの設定とかは私はしてないので、そこの解説は飛ばして、以下のコードだと

setDefaults: を使用して、これらの値を Remote Config オブジェクトに追加します。次の例では、plist ファイルからアプリ内デフォルト値を設定します。

remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults")

アプリで使用するパラメータ値を取得する:
ここまでの手順で、パラメータ値を Remote Config オブジェクトから取得できるようになりました。後で Remote Config バックエンドに値を設定し、フェッチして有効化すると、それらの値をアプリで使用できるようになります。または、setDefaults: を使用して、構成したアプリ内パラメータ値を取得します。アプリ内パラメータ値を取得するには、configValueForKey: メソッドを呼び出します。このとき、引数としてパラメータキーを指定します。

値をフェッチして有効にする:
Remote Config からパラメータ値をフェッチするには、fetchWithCompletionHandler: または fetchWithExpirationDuration:completionHandler: メソッドを呼び出します。バックエンドに設定したすべての値がフェッチされ、Remote Config オブジェクトにキャッシュ保存されます。

1 回の呼び出しで値をフェッチして有効化する場合は、fetchAndActivateWithCompletionHandler: を使用します。

この例では、キャッシュに保存された値の代わりに Remote Config バックエンドから値をフェッチし、それらの値をアプリで使用できるようにするために activateWithCompletionHandler: を呼び出します。

今回はfetchを使いました

remoteConfig.fetch { (status, error) -> Void in
  if status == .success {
    print("Config fetched!")
    self.remoteConfig.activate { changed, error in
      // ...
    }
  } else {
    print("Config not fetched")
    print("Error: \(error?.localizedDescription ?? "No error available.")")
  }
  self.displayWelcome()
}

最後に

今回はSwiftUIで、RemoteConfigを使用して、強制アップデートの機能を実装して見ました。入門レベルなので、機能自体は未完成なところはありますが、誰かのお役て立てると嬉しいです。

Discussion