Firebase 远程配置 iOS 教程

来源:互联网 发布:网络购物的发展 编辑:程序博客网 时间:2024/04/27 21:27

原文:Firebase Remote Config Tutorial for iOS
作者:Todd Kerpelman
译者:kmyhy

记得发布 App 的时候吗?App 每个方面都已经做到最好了吗?你永远不需要碰别的代码了,因为在第一次提交时你就已经做到完美无缺了?

不,我不敢说。

事实是,作为一个功成名就的开发者,通常意味着对 App 没完没了地修改。有时候这种修改是为了增加功能或修复 Bug。但有时候,影响最大的更新无非是一行代码的事儿,比如调整某段文字,或者降低某个塔防游戏中能量单位。

这些改变都很轻松,但发布它们仍然不得不等待许多天。有什么好办法能够做一些细微的修改而不用完成整个流程?

Firebase 远程配置带给你这种方便。在这篇 Firebase 远程配置教程中,你会拿一个 Planet Tour App 为例,学习如何实时修改文字、颜色及其他属性,而不需要发布新包。熟悉之后,你可以使用一些更强大的特性,比如针对不同用户发布不同的内容套装。

前提:本文假设你熟悉和安装了 CocoaPods。否则,请参考我们的 CocoaPods 教程。

运行示例 App

在开始本教程的学习之前,请下载和运行 Planet Tour Starter app。你看可以转动视图,观察不同的行星,并点击它们获得详细数据(非常精确)。

在 Planet Tour Apps 的公司,App 运行得非常良好,直到某天市场部的 Greg 决定将 Planet Tour 换成绿色主题以庆祝地球日。(呃,你可以想象一下,你正在地球日即将来临前的某一天阅读本文。)

这个真的太简单了——如果你看一眼 AppConstants.swift,那里有一个appPrimaryColor 变量,你可以修改它,它会影响所有文字标签的颜色。将这个更新推给你的用户可能需要发布一个新包,提交商店,通过 App 评审,祈祷你所有的用户会在地球日之前下载它。等地球日一过,你又得全部重来一遍。

如果能够从云端改变这些值就好了!

安装远程配置库

这正好是 Firebase 远程配置的最佳案例。冲进附近的时光机,让我们来代替你做出决定,将你的 App 用远程配置取代 AppConstants 中的硬编码。

首先,你需要在 Firebase 控制台中创建一个项目,将它和 Planet Tour App 关联上,然后安装 Firebase 远程配置库。

让我们一步步来:

  1. 打开 firebase.google.com/console
  2. 点击创建新项目。
  3. 将项目命名为 Plannet Tour,确保选择你所在的地区,然后点击创建项目。

  4. 然后,点击 Add Firebase to your iOS app。

  5. 填入你的 Bundle Id(com.razeware.Planet-Tour) ,App Store ID 一栏保留为空。然后点 Add App。

  6. 这时,浏览器会下载 GoogleServices-info.plist 文件。将这个文件拖到 Xcode 项目中(选中 Copy Items if Needed)。
  7. 剩下来的步骤就是在设置向导中点击 Continue。(别担心,后面我们会带你完成这个步骤)。
  8. 在 Xcode 中关闭 Planet Tour 项目。
  9. 打开终端,进入项目目录,输入 pod init 创建一个基本的 Podfile 文件。
  10. 编辑 Podfile :

    target 'Planet Tour' do# Comment this line if you're not using Swift and don't want to use dynamic frameworksuse_frameworks!# Pods for Planet Tourpod 'Firebase/Core'pod 'Firebase/RemoteConfig'end
  11. 执行 pod install,然后用 Xcode 打开 Planet Tour.xcworkspace in Xcode。

  12. 打开 AppDelegate.swift,在 import UIKit 后面添加:

    import Firebase

    然后在 application(_:didFinishLaunchingWithOptions:) 方法返回语句之前添加:

    FIRApp.configure()

    这句代码会找到你安装的库,并用添加 GoogleServices-info.plist 时导入到项目的常量来配置它。远程配置库知道在互联网的某个地方来找到新的值。

运行程序。程序看起来和先前没有任何区别,但在控制台中你会看到一些之前没有见过的调试信息。

注:你可能会看到类似 FIRInstanceID/WARNING STOP!! Will reset deviceID from memory 这样的错误。在 Xcode 8 中,你需要在试用 FireBase 的时候开启钥匙串共享即可消除这个错误。

关系你!你已经安装好了远程配置库!在接下来的教程中你可以使用它了。

使用远程配置

如果你想知道远程配置是如何工作的,最简单的办法是把它看成是一个放在云端的 NSDictionary。当 App 启动时,它会从云端获取新值,并在应用你默认指定的旧值之前优先使用它们。远程配置的正常工作流程如下:

  1. 以 defaults 形式将你有可能在未来改变的值提供给远程配。
  2. 从云端抓取新值。这些会保存到设备的缓存中。
  3. “激活”这些值。你可以看成,抓取到的值被应用到原来的 defaults 值上。
  4. 向远程配置索要值。远程配置要么给你从云端获取到的值(如果有的话),要么给你默认值。

]

值得注意的一点是,从云端抓取的新值通常是你指定的默认值的子集。你可以使用 App 中任何硬编码的字符串、数字或布尔值,并将它们用远程配置连接起来。这种方式很灵活,你可以在未来改变 App 的任何一个方面,但保持你的网络接口整洁、灵巧。

课本念完了。接下来要进行实际操作。
首先,在 Xcode 中打开 Utilities 文件夹,用右键创建新文件。选择 Swift 文件,就叫做 RCValues.swift,将它保存在 Xcode 默认的文件夹。

编辑它的内容为:

import Foundationimport Firebaseclass RCValues {  static let sharedInstance = RCValues()  private init() {    loadDefaultValues()  }  func loadDefaultValues() {    let appDefaults: [String: NSObject] = [      "appPrimaryColor" : "#FBB03B" as NSObject    ]    FIRRemoteConfig.remoteConfig().setDefaults(appDefaults)  }}

RCValues 类使用了单例模式。在 loadDefaultValues() 方法中,你将一个键值对以 defaults 的形式传递给远程配置。暂时你只提供了一个值,但无需担心,后面会添加更多的值。

然后,让远程配置从云端抓取新值。在 init 方法的最后加入:

fetchCloudValues()

在 loadDefaultValues() 方法后新增方法,用于抓取新值:

func fetchCloudValues() {  // 1  // WARNING: Don't actually do this in production!  let fetchDuration: TimeInterval = 0  FIRRemoteConfig.remoteConfig().fetch(withExpirationDuration: fetchDuration) {    [weak self] (status, error) in    guard error == nil else {      print ("Uh-oh. Got an error fetching remote values \(error)")      return    }    // 2    FIRRemoteConfig.remoteConfig().activateFetched()            print ("Retrieved values from the cloud!")  }}

代码解释如下:

  1. 默认,远程配置会缓存从云端获取的值大约 12 个小时。在真实 App 中,这可能够了。但对于你现在要干的事情来说(或者后续一个远程配置教程),这就麻烦大了。因此,你在这里指定了 fetchDuration 为 0,这样永远不会采用缓存数据。
  2. 在完成闭包中,你立即激活这些新抓取到的值,例如,你告诉远程配置,如果一旦发现有新值,立即覆盖旧值。

现在,你在一开始加入的代码出现一点问题。远程配置库有一个客户端的限制,避免你频繁地 ping 服务器。因为你将 fetchDuration 设为了 0,这就违反了这个限制,库会停止调用。

启用开发者模式可以解决这个问题。在 fetchCloudValues() 项目添加方法:

func activateDebugMode() {  let debugSettings = FIRRemoteConfigSettings(developerModeEnabled: true)  FIRRemoteConfig.remoteConfig().configSettings = debugSettings!}

通过设置开发者模式为 true,你告诉远程配置忽略客户端限制。对于测试使用,或者在 10 个人范围内进行测试,这就够了。但如果你将 App 放到公网上,给你的成千上万的粉丝用,你很快就违反了服务端限制,远程库会停止工作。(这就是首先需要在客户端进行一个限制的原因)。

无论如何,请你在将 App 推到生产之前,一定要关闭开发者模式,并设置你的 fetchDuration 为一个合理的值,比如 43200(即 12 小时)。

最后,在设置完 fetchDuration 之后添加:

activateDebugMode()

这句激活调试模式,你不会再出现服务端限制问题。

好,让代码跑起来。打开 AppDelegate.swift, 在 application(_:didFinishLaunchingWithOptions:) 方法中, FIRApp.configure() 一句的后面添加:

let _ = RCValues.sharedInstance

运行程序,在控制台中你会看到:

Retrieved values from the cloud!

注:写到这里的时候,在 Xcode 8 模拟器中,花了很长时间(大概 45 秒)才打出这句。如果你在真机上,速度会快得多。

使用远程配置的值

你已经下到了这些值,将它们打印出来吧。在 fetchCloudValues() 方法的打印 “Retrieved values from the cloud” 的一行后添加:

print ("Our app's primary color is     \(FIRRemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor"))")

这将获取 appPrimaryColor 的值。
运行程序,你会看到:

Our app's primary color is <FIRRemoteConfigValue: 0x61000003ece0>

呃,这其实没什么鸟用,你其实更希望获得一个字符串值。

远程配置将收到的值封装为 FIRRemoteConfigValue 对象,可以看成是某种底层数据的封装,本质上是一种 UTF8 编码的字符串)。你几乎不会直接使用这种类型,而是调用 numberValue/boolValue 之类的助手方法转为你想要的类型。

将刚刚添加的这行为:

print ("Our app's primary color is     \(FIRRemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor").stringValue)")

运行程序,这次你会看到:

Our app's primary color is Optional("#FBB03B")

这才像话,远程配置把你在前面配置的 default 值提供给你了。

修改云端值

现在,你从远处配置获得了正确的值,让我们来试一下修改云端的值。
打开 Firebase 控制台,点击 header 左边的 Remote Config。点 Add your first parameter。在表单中输入一个键叫做 appPrimaryColor,值输入市场部的 Greg 指定的绿色 — #36C278。

点 Add Parameter, 再点 Publish Changes 去更新修改。
运行程序,看一眼控制台:

Our app’s primary color is Optional("#36C278")

哇,现在你已经从云端修改了值!

修改 App 的样式和风格

Xcode 控制台的最新输出确实让我们激动了好一会儿,你的读者们则要理智的多。让我们来看看如何在 App 中应用新值。

首先,加一个每句类型,用于表示这些键吧。直接用字符串作为 key 简直是一场灾难——它至少会让你花费一个下午才能够找出拼写错误的键名。通过枚举,Xcode 会在编译时提示错误,而不是要等到运行时。
打开 RCValues.swift 在类定义之前加入:

enum ValueKey: String {  case appPrimaryColor}

然后,修改 loadDefaultValues() 方法以使用新枚举替换字符串常量:

let appDefaults: [String: NSObject] = [  ValueKey.appPrimaryColor.rawValue : "#FBB03B" as NSObject]

为 RCValues 添加助手方法,接受一个 ValueKey 参数并返回从远程配置中获取到的颜色:

func color(forKey key: ValueKey) -> UIColor {  let colorAsHexString = FIRRemoteConfig.remoteConfig()[key.rawValue].stringValue ?? "#FFFFFFFF"  let convertedColor = UIColor(rgba: colorAsHexString)  return convertedColor}

最后,将 App 中使用到 AppConstants 值的地方用新的 RCValues 助手方法替代。总共有三个地方:

  1. 打开 ContainerViewController.swift, 在 updateBanner() 方法中,将:

    bannerView.backgroundColor = AppConstants.appPrimaryColor

    修改为:

    bannerView.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
  2. 打开 GetNewsletterViewController.swift, 在 updateSubmitButton() 方法中,将:

    submitButton.backgroundColor = AppConstants.appPrimaryColor

    修改为:

    submitButton.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
  3. 打开 PlanetDetailViewController.swift, 在 updateLabelColors() 方法中,将:

    nextLabel.textColor = AppConstants.appPrimaryColor

    修改为:

    nextLabel.textColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)

    最后, 打开 AppConstants.swift 删掉这句:
    static let appPrimaryColor = UIColor(rgba: “#36C278”)

拜拜了,硬编码……

运行程序,你会看到你的 App 现在是绿色了。

在应用这些新值的时候,你没有多少控制权。第一次运行 App 时,你会看见主菜单是默认的橙色,当你从云端加载新值之后,行星详情显示为绿色。

这可能会给用户带来困惑。在这种情况下,你只改变了部分 Label 的颜色,如果用户正在使用中而 App 恰巧正在改变颜色或者某些会影响到它的行为的值时,用户会更奇怪。

有无数种解决办法。但最简单的方法是使用一个 loading 界面。幸运的是,已经有一个现成的(至少是部分)摆在那儿了。

使用 Loading 页

首先你需要让 Loading 页面作为 App 的第一个 View Controller。打开 Main.storyboard ,右键,从你的导航控制器拖到 Waiting View Controller — 这个 View Controller 有一个黑背景,当然你也可以在故事版的 Outline 窗口中来右键拖放,这个更容易些。在弹出菜单中选择 root view controller:这将使你的 Loading 页变成 App 启动时的第一个画面。

现在,来加一些代码使得当远程配置完成抓取后跳转到主菜单页面。
打开 RCValues.swift, 在 sharedInstance 属性后添加:

var loadingDoneCallback: (() -> ())?var fetchComplete: Bool = false

在 fetchCloudValues() 完成块的最后加入两行代码:

func fetchCloudValues() { // 警告: 不要在生产项目中这样做! let fetchDuration : TimeInterval = 0 activateDebugMode() FIRRemoteConfig.remoteConfig().fetch(withExpirationDuration: fetchDuration) {   [weak self] (status, error) in   guard error != nil else {     print ("Uh-oh. Got an error fetching remote values \(error)")     return   }      FIRRemoteConfig.remoteConfig().activateFetched()   print ("Retrieved values from the cloud!")   print ("Our app's primary color is     \(FIRRemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor").stringValue)")   // 在这里加入两行!   self?.fetchComplete = true   self?.loadingDoneCallback?() }}

这里,当抓取完成时,将 fetchComplete 变量设为 true。然后调用回调块通知监听者远程配置新值已经抓取完了。这用于告诉 Loading 页面将自己解散。

打开 WaitingViewController.swift 新增方法:

func startAppForReal() {  performSegue(withIdentifier: "loadingDoneSegue", sender: self)}将 viewDidLoad() 替换为:```swiftoverride func viewDidLoad() {  super.viewDidLoad()  if RCValues.sharedInstance.fetchComplete {    startAppForReal()  }  RCValues.sharedInstance.loadingDoneCallback = startAppForReal}<div class="se-preview-section-delimiter"></div>

这里,当所有值都已经抓取完成后,调用了 stratAppFroReal() 方法。这里也进行了另一种判断,即在 Loading 屏已经被加载之前,RCValues 是否已经完成了网络抓取。这种情况其实是不可能发生的,但加上它只会让你的代码更健壮。

我有一个编码规则:当你在代码中注释“这种情况永远不会发生”的时候,恰恰表明未来很可能就会发生。

运行程序。你会看到 Loading 屏会短暂显示(取决于你的网速),然后就跳到 App 的其他界面了。如果你修改了 App 的主题颜色并重启 App,这个颜色就会在整个 App 中都得到正确应用了。

修改 App 的其它部分

现在你已经将 一个 AppConstants 中的值转到 RCValue 了,你可以继续转化其他值!打开 RCValues.swift 将 ValueKey 修改为:

enum ValueKey: String {  case bigLabelColor  case appPrimaryColor  case navBarBackground  case navTintColor  case detailTitleColor  case detailInfoColor  case subscribeBannerText  case subscribeBannerButton  case subscribeVCText  case subscribeVCButton  case shouldWeIncludePluto  case experimentGroup  case planetImageScaleFactor}<div class="se-preview-section-delimiter"></div>

然后,将 loadDefaultValues() 方法修改为:

func loadDefaultValues() {  let appDefaults: [String: NSObject] = [    ValueKey.bigLabelColor.rawValue: "#FFFFFF66" as NSObject,    ValueKey.appPrimaryColor.rawValue: "#FBB03B" as NSObject,    ValueKey.navBarBackground.rawValue: "#535E66" as NSObject,    ValueKey.navTintColor.rawValue: "#FBB03B" as NSObject,    ValueKey.detailTitleColor.rawValue: "#FFFFFF" as NSObject,    ValueKey.detailInfoColor.rawValue: "#CCCCCC" as NSObject,    ValueKey.subscribeBannerText.rawValue: "Like Planet Tour?" as NSObject,    ValueKey.subscribeBannerButton.rawValue: "Get our newsletter!" as NSObject,    ValueKey.subscribeVCText.rawValue: "Want more astronomy facts? Sign up for our newsletter!" as NSObject,    ValueKey.subscribeVCButton.rawValue: "Subscribe" as NSObject,    ValueKey.shouldWeIncludePluto.rawValue: false as NSObject,    ValueKey.experimentGroup.rawValue: "default" as NSObject,    ValueKey.planetImageScaleFactor.rawValue: 0.33 as NSObject  ]  FIRRemoteConfig.remoteConfig().setDefaults(appDefaults)}<div class="se-preview-section-delimiter"></div>

然后,新增 3 个助手方法,以便能够获取 UIColor 之外的其他类型的值:

func bool(forKey key: ValueKey) -> Bool {  return FIRRemoteConfig.remoteConfig()[key.rawValue].boolValue}func string(forKey key: ValueKey) -> String {  return FIRRemoteConfig.remoteConfig()[key.rawValue].stringValue ?? ""}func double(forKey key: ValueKey) -> Double {  if let numberValue = FIRRemoteConfig.remoteConfig()[key.rawValue].numberValue {    return numberValue.doubleValue  } else {    return 0.0  }}

接着将 App 中凡是用到 AppConstants 值的地方全部替换为对应的 RCValue 值。你可以这样:

  1. 打开 ContainerViewController.swift, 将 updateNavigationColors() 改成:

    func updateNavigationColors() {navigationController?.navigationBar.tintColor = RCValues.sharedInstance.color(forKey: .navTintColor)}
  2. 将 updateBanner() 改成:

    func updateBanner() {bannerView.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)bannerLabel.text = RCValues.sharedInstance.string(forKey: .subscribeBannerText)getNewsletterButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeBannerButton), for: .normal)}
  3. 打开 GetNewsletterViewController.swift, 将 updateText() 改成:

    func updateText() {instructionLabel.text = RCValues.sharedInstance.string(forKey: .subscribeVCText)submitButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeVCButton), for: .normal)}
  4. 打开 PlanetDetailViewController.swift, 将 updateLabelColors() 中的这一行:

    nextLabel.textColor = AppConstants.detailInfoColor

    修改为:

    nextLabel.textColor = RCValues.sharedInstance.color(forKey: .detailInfoColor)
  5. 将这一行:

    planetNameLabel.textColor = AppConstants.detailTitleColor

    修改为:

    planetNameLabel.textColor = RCValues.sharedInstance.color(forKey: .detailTitleColor)
  6. 打开 PlanetsCollectionViewController.swift, 在customizeNavigationBar() 中将这行:

    navBar.barTintColor =  AppConstants.navBarBackground

    替换为:

    navBar.barTintColor =  RCValues.sharedInstance.color(forKey: .navBarBackground)
  7. 在 collectionView(_:cellForItemAt:) 中, 将这行:

    cell.nameLabel.textColor = AppConstants.bigLabelColor

    替换为:

    cell.nameLabel.textColor = RCValues.sharedInstance.color(forKey: .bigLabelColor)
  8. 打开 SolarSystem.swift, 在 init() 中, 将这一句:

    if AppConstants.shouldWeIncludePluto {替换为:```swiftif RCValues.sharedInstance.bool(forKey: .shouldWeIncludePluto) {
  9. 最后, 在 calculatePlanetScales() 中将这句:

    scaleFactors[i] = pow(ratio, AppConstants.planetImageScaleFactor)

    替换为:

    scaleFactors[i] = pow(ratio, RCValues.sharedInstance.double(forKey: .planetImageScaleFactor))

嘘! 好了,改完这么多地方,你可以来确认一下你的 App 是否都改完了。这样,你可以搜索 App 中的所有 “AppConstants” — 你只会找到一个结果,那就是它自己的结构定义。

如果你还不信,你可以把 AppConstants 文件删除。你的 App 依然能够编译,没有任何问题。

现在,你的 App 已经完全支持远程配置了,你可以继续实现 Gary 要求你做的任何其它改变。

打开 Firebase 控制台。在 Remote Config 节点击 Add Parameter。key 输入 navBarBackground ,value 输入 #35AEB1,点击 Add Parameter。重复重样动作将 navTintColor 设为 #FFFFFF。点击 Publish Changes ,将值下发给 App。

你的 Firebase 控制台最终将是这个样子:

App 则是这个样子:

你可以任意调整,试试修改其它值。胡乱改几个文字内容。尝试各种样式……花色……颜色。

但当你做完这一切之后,请回到本教程,这里还有一个严重的问题有待解决!

冥王星回归

事情就坏在丹麦人身上!当世界上大部分人都已经接受冥王星不再是一颗行星的同时,北欧保护冥王星协会,一个由狂热的冥王星迷组成的组织,顽固地坚持冥王星作为一颗行星存在,并应当将它放到 Planet Tour App 中。在你阅读本文的同时,哥本哈根的街道上排满了抗议的人群!怎么办?

重新发布一个 App,则会激怒另一群人……

好吧,使用远程配置,这好像不是太难!你可以将 shouldWeIncludePluto 设为 true。等等,这回改变所有用户的设置,而不仅仅是北欧。怎样才能基于不同的地区下发不同的设置?

答案是 Conditions!

远程配置比起简单的云端字典来说更加智能,那就是根据不同的人群发布不同的设置。你可以利用这个特性允许北欧用户重新迎回它们的冥王星。

首先,打开 Firebase 控制台,在 Remote Config 面板中,点击 Add Parameter 添加一个新参数。
key 输入 shouldWeIncludePluto。
点击 value 栏旁边的 Add value for condition 下拉框。选择 Define New Condition。

在对话框中,给新条件命名为 Pluto Fans。

在下拉框中,选择 Device Region / Country。

在国家列表中,选择 Denmark, Sweden, Norway, Iceland, 和 Finland。

点击 Create Condition。

然后,在 Value for Pluto Fans 栏,输入值 true。在 Default value 栏输入 false。

最后,点击 Add Parameter,再点击 Publish Changes。

运行程序,假设你没有在这些北半球国家,你仍然不能在行星列表中看见冥王星。如果你想体验一下北欧用户,我建议你买一张到哥本哈根的机票,买一部丹麦版的 iPhone,然后打开 App,顺便来一块熏鲑鱼单片三明治。

有一个更经济的做法(同时更少的时差)是,打开设备后模拟器上的设置程序。选择 General > Language & Region > Region > Denmark (或其它北欧国家)

这比飞到哥本哈根要便宜得多了,但同时也少了许多乐趣。

运行程序,这次你可以看见冥王星和别的行星列在一起。呼,避免了一起国际纠纷!

结束语

你可以从这里下载最终项目。但是请注意,你仍然需要在 Firabase 控制台中创建项目,并将你的 GoogleServices-info.plist 文件拖到项目中。

通过远程配置能让你实现许多功能。如果你在开发游戏,如果玩家觉得难度过低或过高,用它来调整游戏玩法是一种好办法。还可以用它来实现“每日提醒”之类的功能。甚至可以用它来实验不同的按钮和标签文本,看看哪种能够让用户体验最好。在你的 App 中试试吧,看看你能改变些什么?

有很多特性还没有来得及展示。通过将值下发给随机的用户组,你可以用远程配置来进行 A/B 测试,或者逐步将新功能推广到其他地区。你还可以将不同的数据集下发给通过 Firebase Analytics 识别出的某个用户组,实现某种定制化效果。如果你想进一步了解,请阅读这个文档。

如果你有任何关于 Firebase 远程配置(或者 Planet Tour 颜色方案)的问题或建议,请在下面留言!

0 0