Firebase Remote Configの機能を共通化した構造体を作ってみた(Swift)


Firebase Remote Configとは

WebでいうGoogleAnalyticsのモバイル版がFirebaseという感じです。GoogleAnalyticsほどさまざまな分析は現状まだできませんが、分析の他にもプッシュ通知(Notification)や、モバイル端末で持っているプロパティをリモートで操作する(RemoteConfig)ことが可能です。表示している色などをクライアントによってランダムに変更するというのもRemoteConfigを利用すると簡単にできます(ABテストですね)。

やりたいこと

ユーザによって表示するコンテンツを変えたい。最新バージョンの人とそうでない人とでコンテンツを変えてみます。

環境

Xcode7.3, Swift2.2

実際にRemote Configを使ってみた

1, コンソールでの設定

まずFirebaseのコンソール画面に行き、Remote Configの設定をしていきます。パラメータを追加をクリックして必要なパラメータを作成します。ここでは最新バージョンをいれているユーザに違う値を渡すようにするため、latestVersionという条件を作成しています。

2, 実装

Podfileに pod 'Firebase/RemoteConfig' と記入して pod install とターミナルで実行してFirebaseRemoteConfigをインストールします。

AppDelegate.swiftは最低限以下のようになります。(githubより抜粋)

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions   launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // [START configure_firebase]
        FIRApp.configure()
        // [END configure_firebase]

        return true
    }
}

ViewController.swiftは以下のようになります。

import UIKit
import Firebase
import FirebaseRemoteConfig

class ViewController: UIViewController {

    var remoteConfig: FIRRemoteConfig!
    var tabTitles: [String] = ["ヘアアレンジ", "メイク", "スキンケア"]
    var tabTitlesEn: [String] = ["hair_arrange", "make", "skin_care"]

    override func viewDidLoad() {
        super.viewDidLoad()

        setTabWithRemoteConfig() 

    }

    ...

    func setTabWithRemoteConfig() {

        // RemoteConfigのシングルトンインスタンス取得
        remoteConfig = FIRRemoteConfig.remoteConfig()

        // デバッグモードの設定
        let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: true)
        remoteConfig.configSettings = remoteConfigSettings!

        // 下でデフォルト値を設定しているからこれはなくても問題ない.
        // remoteConfig.setDefaults(["tabTitles": "アイテム"])
        // remoteConfig.setDefaults(["tabTitlesEn": "item"])

        // キャッシュの有効期間
        let expirationDuration = remoteConfig.configSettings.isDeveloperModeEnabled ? 0 : 3600

        // Firebaseのコンソール画面で設定した値を持ってくる非同期処理
        remoteConfig.fetchWithExpirationDuration(NSTimeInterval(expirationDuration)) { [weak self] (status, error) -> Void in

            if (status == FIRRemoteConfigFetchStatus.Success) {

                self?.remoteConfig.activateFetched()
            } else {
                print("Config not fetched")
                print("Error \(error!.localizedDescription)")
            }

            // 値を取り出す. ここでデフォルト値も設定.
            self?.tabTitles.append(self?.remoteConfig["tabTitles"].stringValue ?? "アイテム")
            self?.tabTitlesEn.append(self?.remoteConfig["tabTitlesEn"].stringValue ?? "item")

            // tableViewを扱っている時はここでreloadDataをする.
            // 値をRemote Configで取得した後の処理を実装する.
        }
    }

    ...

}

この処理をどこかにまとめたい

Remote Configを利用する時に、毎回これを書くと同じコードが増えるので、FirebaseRemoteConfigManagerを作って同じコードになりうるところは一箇所にまとめようと思います。

FirebaseRemoteConfigManager.swiftの作成

BrightFuturesを使って非同期処理を実装していきます。Carthage等でBrightFuturesをインストールしておきます。以下はFirebaseRemoteConfigManager.swiftの中身です。

import Foundation
import Firebase
import FirebaseRemoteConfig
import BrightFutures

struct FirebaseRemoteConfigManager {

    private let remoteConfig: FIRRemoteConfig
    private let expirationDuration: Int

    init() {
        // さっきfunctionの中で設定していたものをイニシャライザにまとめる.
        self.remoteConfig = FIRRemoteConfig.remoteConfig()

        let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: true)
        self.remoteConfig.configSettings = remoteConfigSettings!

        self.expirationDuration = remoteConfig.configSettings.isDeveloperModeEnabled ? 0 : 3600

    }

    // 返り値はFutureでラップする.
    func fetchRemoteConfig() -> Future<FIRRemoteConfig, NSError> {

        let promise = Promise<FIRRemoteConfig, NSError>()

        remoteConfig.fetchWithExpirationDuration(NSTimeInterval(expirationDuration)) { (status, error) -> Void in

            if let error = error {
                promise.failure(error)

                // 処理に失敗したから早めにreturnさせる.
                return 
            }

            if status != FIRRemoteConfigFetchStatus.Success {
                let error = NSError(domain: "hogehoge", code: 0, userInfo: [NSLocalizedDescriptionKey: "status is failed"])
                promise.failure(error)

                // 処理に失敗したから早めにreturnさせる.
                return
            }

            self.remoteConfig.activateFetched()

            // 通信が問題なく完了した時のみSuccessになる.
            promise.success(self.remoteConfig)

        }

        return promise.future

    }

}

ViewControllerの実装

import UIKit
import Firebase
import FirebaseRemoteConfig

class ViewController: UIViewController {

    var tabTitles: [String] = ["ヘアアレンジ", "メイク", "スキンケア"]
    var tabTitlesEn: [String] = ["hair_arrange", "make", "skin_care"]

    // FirebaseRemoteConfigManagerをインスタンス化する.
    private let remoteConfigManager = FirebaseRemoteConfigManager()

    override func viewDidLoad() {
        super.viewDidLoad()

        setTabNameFromRemoteConfig() 

    }

    ...

    private func setTabNameFromRemoteConfig() {
        // .onSuccessに、非同期通信がうまく行った時の(Remote Configで設定した値を利用する)処理を書く.
        // .onFailureに、非同期通信が失敗した時の処理を書く.
        remoteConfigManager.fetchRemoteConfig()
            .onSuccess { result in
                print("Fetch succeeded.")

                // ["key名"].stringValueで値を取り出せる.
                self.tabTitles.append(result["tabTitles"].stringValue ?? "アイテム")
                self.tabTitlesEn.append(result["tabTitlesEn"].stringValue ?? "item")

                // tableViewを扱っている時はここでreloadDataをする.
                // 値をRemote Configで取得した後の処理を実装する.
            }
            .onFailure { error in
                print(error.localizedDescription)
            }
    }

    ...
}

ViewControllerの記述量が減った上、コールバックで書くと深いネストになる問題点も回避できます。Remote Configを使いたいところで、private let remoteConfigManager = FirebaseRemoteConfigManager()を記述して、以下のコードをfuncの中などにいれてあげれば良いです。

remoteConfigManager.fetchRemoteConfig()
    .onSuccess { result in
        // ["key名"].stringValueで値を取り出せる.
        // result["key名"].stringValue ?? "デフォルト値など"を使った処理
        // tableViewを扱っている時はここでreloadDataをする.
    }
    .onFailure { error in
        print(error.localizedDescription)
    }