Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

Firebase RemoteConfigをSwiftyに扱うライブラリ作ってみた

お久しぶりです。iOSエンジニアのすーです。
チームメンバーがFirebase、Realm Mobile Platformであれこれ試行錯誤しているのに触発され、遅れてのスタートですが僕もFirebaseのRemoteConfigについて触っていくことにしました。

今回はFirebase RemoteConfig × Swiftなお話です。 (※ Xcode 8,Swift 3時点のものです。)

Pickup:

techblog.timers-inc.com techblog.timers-inc.com

RemoteConfigってどんなの?

Remote Configとは、Firebaseで提供されているサービスの1つで、クラウド上で値を設定し、それをネイティブ側から設定値をフェッチして動的にアプリ内の数値等を変えることができるものです。
また、セグメントや細かい条件設定によって設定値を出し分けることができるので、A/Bテストをするのにも向いていると思います。

実装も、こんな感じでとても簡単にクラウドから値を取得して反映することができます。

self.remoteConfig.fetchWithExpirationDuration(expirationDuration) {
    (status, error) in
    if status == .Success {
        print("Config fetched!")
        self.remoteConfig.activateFetched()
        self.displayPrice()
    } else {
        print("Config not fetched")
        print("Error \(error!)")
        let units = self.remoteConfig[kPricePrefixConfigKey].stringValue!
        let price = self.remoteConfig[kPriceConfigKey]).numberValue!
        self.priceLabel.text = "\(units)\(price)"
    }
}

func displayPrice() {
    let units = self.remoteConfig[kPricePrefixConfigKey].stringValue!
    if self.remoteConfig[kIsPromotionConfigKey].boolValue {
        // [START get_config_value]
        let discountedPrice = self.remoteConfig[kPriceConfigKey].numberValue!.longValue -
                                self.remoteConfig[kDiscountConfigKey].numberValue!.longValue;
        // [END get_config_value]
        self.priceLabel.text = "\(units)\(discountedPrice)"
    } else {
        let price = self.remoteConfig[kPriceConfigKey]).numberValue!
        self.priceLabel.text = "\(units)\(price)"
    }
}

(Firebaseのサンプルコードから引用)

しかし、、、

値の取得、デフォルト値のセットがなんだかイマイチ

元がObjective-Cで書かれていることもあって、Swiftだと一部使いづらい箇所がでてきます。
デフォルト値をセットする時に、FIRRemoteConfigクラスのsetDefaults: でDictionaryを渡すのですが、このDictionaryの型が [String: NSObject] なので、StringやInt,Boolをそのまま渡せません。。
一度NSStringNSNumberに包まないといけないようです。面倒...

(※これに関しては、Swift2までは暗黙的に変換された可能性がありますが、Swift3からは包むなり、 as NSObjectと書かないとだめになりました。)

let remoteConfig = FIRRemoteConfig.remoteConfig()
remoteConfig.setDefaults([
    "test_label": NSString(string: "Hello, World!")
    "test_num": NSNumber(value: 123)
])

ちなみに、plistから値をセットする方法もありますが、plistって外から見られる可能性もあるため、できれば避けたいですね。

また、値の取得時は、ある程度自由に型を変えることができてしまうため、型にうるさいSwiftっぽさがないです。

// "test_label"で取得できるものは "Hello, world"でStringを想定
let remoteConfig = FIRRemoteConfig.remoteConfig()
let text = remoteConfig["test_label"].stringValue
let num =  remoteConfig["test_label"].numberValue?.intValue // クラッシュしないかもしれないけど気持ち悪い...

可能なら、"test_label" と取得する時、デフォルト値をセットする時は "String"型であると担保したいところです。

そこで...

型安全に、Swiftyに扱えるもの、作ってみた

ということで、型安全に、Swiftyっぽく扱えるライブラリを作ってみました。

github.com

これを使うとどうなるのか、Before/Afterを踏まえて紹介してみます。

// - デフォルト値の設定
// before
let remoteConfig = FIRRemoteConfig.remoteConfig()
remoteConfig.setDefaults([
    "test_label": "Hello, World!" as NSString
    "test_num": NSNumber(value: 123)
])

// after
extension ConfigKeys {
    static let testLabel = ConfigKey<String>("test_label")
    static let testNum = ConfigKey<Int>("test_num")
}
Shrimp.shared.config[.testLabel] = "Hello, World!"
Shrimp.shared.config[.testLabel] = 123 // コンパイルエラー!
Shrimp.shared.config[.testNum] = 123
Shrimp.shared.config[.testNum] = "Hello, World!" // コンパイルエラー!

// - 値の取得
// before
let text = remoteConfig["test_label"].stringValue ?? ""
let num = remoteConfig["test_num"].numberValue?.intValue ?? 0
let num: Int = remoteConfig["test_label"].numberValue?.intValue ?? 0 //怪しいぞ...
// after
let text = Shrimp.shared.config[.testLabel] // String型として取得できる
let num = Shrimp.shared.config[.testNum] // Int型として取得できる
let num: Int = Shrimp.shared.config[.testLabel] // コンパイルエラー

このように、ConfigKeyを宣言するときに、 <String> と、そのkeyで取得できるvalueの型を指定することで、値の設定/取得時に型安全に扱うことができます。
また、設定/取得の時は、ConfigKeysstatic let として宣言してあげることで、
Shrimp.shared.config[.testLabel] と、ドット記法を使った subscript が可能になります。

また、fetchした後の値ではなくて、デフォルト値を取得したい場合は、
subscript 時に default:を付けてあげるとそのkeyに対応するRemoteConfigにセットしたデフォルト値を取得できます。

//["test_label": "hogehoge"] とクラウドからfetchしたとする

Shrimp.shared.config[.testLabel] = "Hello, World!"

let text = Shrimp.shared.config[.testLabel]  // "hogehoge"
let defaultText = Shrimp.shared.config[default: .testLabel] // "Hello, World!"

また、こんな感じでRemoteConfig側に数値(0,1,2)を設定して、それをIntのenumとして読み込みたいときも、
ちょこっと必要な処理を書くだけで、毎回変換を意識しないで値の設定/取得ができるようになります。

enum SampleType: Int {
    case none, a, b
}

extension ConfigKeys {
    static let testType = ConfigKey<SampleType>("test_type")
}

// subscriptを定義してあげる
extension RemoteConfig {
    subscript (key: ConfigKey<SampleType>) -> SampleType {
        get { return int(for: key).flatMap(SampleType.init) ?? .none }
        set { set(key: key, value: newValue.rawValue) }
    }
}

// デフォルト値の設定
Shrimp.shared.config[.testType] = .none

// 値の取得
switch Shrimp.shared.config[.testType] {
    case .a:
        // ...
    case .b:
        // ...
    case .none:
        // ...
}

RemoteConfigのextensionとして、 ConfigKey<SampleType>に関する subscript を定義してあげます。
get が値を取得するとき、 set が、RemoteConfigへデフォルト値をセットする所になります。

まとめ

クラウド側に設定値を置いておけるし、デフォルト値をネイティブ側でも設定できるし、条件に応じて出し分けたり、その必要がなくなったら条件を取っ払って全展開も容易なRemoteConfigを使わない手はないですね。
そんなときに、よりRemoteConfigをSwiftyに扱えるライブラリShrimpをよかったら使ってみてください!(issue/PRもお待ちしています!)

積極採用中!!

子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる