原文:Firebase Remote Config Tutorial for iOS
作者:Todd Kerpelman
译者:kmyhy
在你发布应用程序的时候,app 各个方面都已经完美了吗?你永远不需要碰其它代码,因为无论什么东西你一次就能做对?
哈,我做不到。
作为一名成功的 app 开发者,通常需要对 app 进行频繁的更改。有时,这些更改是增加功能或者 bug 的修复。但是,有时,最有关键的修改只是一行代码而已,比如调整一下文字或者在一个塔式防御游戏中削弱一个强大的单元。
虽然这些修改很简单,但是发布它却要花一两天的时间。如果你只需要做一些调整,但不需要经历这整个过程就太好了。
Firebase 远程配置提供了这种能力。在这篇基于 Firebase 的 iOS 远程配置教程中,你将使用 Planet Tour app 来学习如何修改文本、颜色和其他行为,而无需发布新版本!掌握了它以后,你将学到更强大的功能,面向不同用户交付不同内容。
前提条件:这篇基于 Firebase 的 iOS 远程配置教程假设您安装了 cocoaPods 并对它有一定的了解。如果不是这样,请查看我们的 CocoaPods 教程。
在本教程的顶部或底部有下载链接,请首先下载。解压并运行开始项目。你可以滑动以查看不同的行星,点击每一个行星来获得一些(大部分是准确的)详情信息。
你刚刚下载的 app 是 Planet Tour APP 公司制作的,它一直都很正常,直到有一天,来自市场营销的格雷格决定,行星之旅变成绿色的配色方案,以庆祝地球日。
如果你打开 AppConstants.swift,你会发现这很容易搞定。有一个 appPrimaryColor变量,修改它会同时改变到许多文本标签的颜色。将改变推给用户需要发布一个新版本,将它提交给应用商店,通过审批,然后等用户在地球日之前下载它。一旦地球日结束,你必须重新做整个过程,恢复之前的样子。
如果能从云端修改这些值,那就好了。
要将 AppConstants 中的硬编码值修改为远程配置,需要在 Firebase 控制台中新建一个项目,将它和 Planet Tour app 关联,然后安装 Firebase 远程配置库。
步骤如下:
将项目名称命名为 Planet Tour,选中你所在的区域,然后点击 Create Project。
点击 Add Firebase to your iOS app:
设置项目的 bundle ID 为 com.razeware.Planet-Tour —— 保持 App Store ID 字段留空,然后点击 Register App:
点击 Download 下载一个 GoogleServices-info.plist 文件:
浏览器会下载一个 GoogleServices-info.plist 文件。将它拖进你的 Xcode 项目中。确保选择 Copy Items if Needed。
不停点击向导中的 Continue 按钮。别怕,接下来你将会看到这些指引。
编辑 Podfile 文件为:
# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'
target 'Planet Tour' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for Planet Tour
pod 'Firebase/Core'
pod 'Firebase/RemoteConfig'
end
执行 pod install, 用 Xcode 打开 Planet Tour.xcworkspace。
打开 AppDelegate.swift.在 import UIKit 之后添加:
import Firebase
然后,在 application(_:didFinishLaunchingWithOptions:) 的 return 语句之前添加:
FirebaseApp.configure()
这个方法检查刚才安装的两个库,并使用你添加在 GoogleServices-info 中的常量来初始化。远程配置库现在知道要去 internet 上查找新值的正确位置。
Build & run,app 和之前一样,但你从控制台中会看到一些之前没有的调试信息。
恭喜你!你已经安装好了远程配置!现在你可以在接下来的教程中使用它了!
非常简单,远程配置通过类似于在云端的 [String:Any] 字典来工作。当你的应用程序启动时,它会从云中抓取任何可能需要的新值,然后将它们应用到你可能指定为默认值的任何旧值之上。
使用远程配置的一般流程是:
有一点要注意,获取到的这些新值通常是你提供的默认值的子集。你可以将应用程序中几乎任何硬编码的字符串、数字或布尔值,打包在一起到远程配置。这让你能够在以后灵活地修改应用程序的许多地方,同时保持网络调用小而美。
理论课完了,来点实际的!
首先,打开 Planet Tour 项目的 Utilities 文件夹,用右键新建文件。选择 Swift 文件,文件名就叫做 RCValues.swift,将它创建在 Xcode 推荐的默认目录下。
在文件中加入:
import Firebase
class RCValues {
static let sharedInstance = RCValues()
private init() {
loadDefaultValues()
}
func loadDefaultValues() {
let appDefaults: [String: Any?] = [
"appPrimaryColor" : "#FBB03B"
]
RemoteConfig.remoteConfig().setDefaults(appDefaults as? [String: NSObject])
}
}
这里,使用了单例模式。在 loadDefaultValues() 中,传递给远程配置一对键值对,充当默认值。这里只提供了一个值,但别急,后面会添加更多值。
然后,请求远程配置从云端返回新值。在 loadDefaultValues() 方法下面添加方法:
func fetchCloudValues() {
// 1
// WARNING: Don't actually do this in production!
let fetchDuration: TimeInterval = 0
RemoteConfig.remoteConfig().fetch(withExpirationDuration: fetchDuration) { status, error in
if let error = error {
print("Uh-oh. Got an error fetching remote values \(error)")
return
}
// 2
RemoteConfig.remoteConfig().activateFetched()
print("Retrieved values from the cloud!")
}
}
这里做了些什么?
默认情况下,远程配置将缓存从云中检索的任何值大约12小时。在一个生产应用中,这可能很好。但是,当你在进行开发或在线使用这篇 Firebase 远程配置教程时,这可能会让测试新值变得非常麻烦。所以,你要指定 fetchDuration 为 0,表示永远不使用缓存数据。
在 completion 闭包中,立即激活抓取到的值 —— 即告诉远程配置将新值替换掉旧值。
在 init() 方法中调用新方法:
fetchCloudValues()
您在开始添加的代码会有一些问题。远程配置库对客户端流量有控制,确保你无法频繁地 ping 服务。将 fetchDuration 设为 0 会和这个控制发生冲突,你的库将停止抓取值。
可以打开开发者模式以解决这个问题。在 fetchCloudValues() 下面添加方法:
func activateDebugMode() {
if let debugSettings = RemoteConfigSettings(developerModeEnabled: true) {
RemoteConfig.remoteConfig().configSettings = debugSettings
}
}
将开发者模式设置为 true,告诉远程配置忽略客户端流量控制。对于开发过程,或者在 10 人以下的测试团队来说,这是没问题的。但是,如果你把这款应用发布到公众面前,你会有数百万的粉丝,你很快就会引起服务器端的节流阀,远程配置会停止工作。这就是你首先要有一个客户端节流阀的原因。
在将 app 推到生产之前,确保禁用开发者模式,并将 fetchDuration 设置为更合理的时间,比如43200,即 12小时。
最后,在 fetchDuration 变量的声明语句之后添加:
activateDebugMode()
这将开启 debug 模式,避免发生服务端节流控制问题。
打开 AppDelegate.swift,在application(_:didFinishLaunchingWithOptions:) 的 FirebaseApp.configure() 之下添加:
let _ = RCValues.sharedInstance
Build & run,你会看到控制台中输出:
Retrieved values from the cloud!
现在你下载了这些值,来将它们输出到控制台。打开 RCValues.swift。在 fetchCloudValues() 的 “Retrieved values from the cloud” 一行后面添加:
print("Our app's primary color is \(RemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor"))")
这句代码将打印 appPrimaryColor 的值。
Build & run。你会开到:
Our app's primary color is
这很好,但你想要一个字符串值。
远程配置将检索到的值当成 RemoteConfigValue 对象,你可以将其看作是包含底层数据的容器,后者在内部表示为 utf8 编码的字符串。你几乎不会直接使用这个对象。相反,你将调用 numberValue 或 boolValue 之类的辅助方法来检索你想要的实际值。
将刚才那句替换成:
let appPrimaryColorString = RemoteConfig.remoteConfig()
.configValue(forKey: "appPrimaryColor")
.stringValue ?? "undefined"
print("Our app's primary color is \(appPrimaryColorString)")
Build & run。这次你会看到:
Our app's primary color is #FBB03B
这会更方便一点。远程配置提供了你之前指定的默认值。
现在你能够从远程配置拿到正确的值了,尝试在云端提供新值吧。
打开 Firebase 控制台,点击顶部左边的 Remote Config (在 Grow 之下)。点击 Add your first parameter。在表单中,将 key 设为 appPrimaryColor ,而值则设置为市场部 Greg 最爱的新绿色 #36C278。
点击 Add Parameter, 然后点击 Publish Changes (两次) 去刷新修改。
Build & run 。
你在控制台中会看到:
Our app’s primary color is #36C278
成功了!你已经修改了云端的值!
现在,来将新值和 app 关联起来。
首先,加一个枚举来保存 key 值。使用常量字符串作为 key 是一种灾难,或者你起码得花一下午的时间来寻找一个神秘的 bug,因为你输错了一个 key。通过使用enum,Xcode可以在编译时捕捉错误,而不是运行时。
打开 RCValues.swift 在类定义之上加入:
enum ValueKey: String {
case appPrimaryColor
}
然后,修改 loadDefaultValues() 方法,使用枚举代替常量字符串:
let appDefaults: [String: Any?] = [
ValueKey.appPrimaryColor.rawValue : "#FBB03B"
]
在 RCValue 中添加一个工具方法,它将一个 ValueKey 作为参数,然后根据这个字符串从远程配置返回一个 UIColor。
func color(forKey key: ValueKey) -> UIColor {
let colorAsHexString = RemoteConfig.remoteConfig()[key.rawValue].stringValue ?? "#FFFFFF"
let convertedColor = UIColor(colorAsHexString)
return convertedColor
}
最后,将 app 中原来使用 AppConstants 常量的地方替换成 RCValues 的工具方法。
有 3 个地方需要改:
打开 ContainerViewController.swift 修改 updateBanner() 方法中的:
bannerView.backgroundColor = AppConstants.appPrimaryColor
为:
bannerView.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
打开 GetNewsletterViewController.swift 将 updateSubmitButton() 方法中的:
submitButton.backgroundColor = AppConstants.appPrimaryColor
修改为:
submitButton.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
打开 PlanetDetailViewController.swift 将 updateLabelColors() 方法中的:
nextLabel.textColor = AppConstants.appPrimaryColor
修改为:
nextLabel.textColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor)
为了完美,打开 AppConstants.swift and delete the following:
static let appPrimaryColor = UIColor(rgba: "#FBB03B")
再见了,硬编码…
Build& run。你会发现整个 app 变成绿色的了:
当这些新值被应用时,你不能做过多控制。当第一次运行 app 时,可能会看到主菜单是默认的橙色,但是当新值从云中加载后,你就会看到行星详情中显示的新绿色。
这可能会让用户感到困惑。在这个例子里,你只是修改了一些标签的颜色,但是如果你的 app 改变了能够影响到它的行为的文本或值怎么办呢?如果用户正在使用的过程中,他们会感到困惑。
有很多方法可以解决这个问题,但是最简单的方法是创建一个加载页面。在本教程中,它已经创建好了一部分。
首先,让加载屏幕成为 app 的 inital View controller。打开 Main.storyboard。右键,从导航控制器拖一条线到 Waiting View Controller —— 它是一个黑背景的视图控制器,你也可以通过 outline 视图来操作,这样可能更容易。从弹出菜单中选择 root view controller:当你的应用加载时,这将使你的加载屏幕成为初始屏幕。
现在,为远程配置加载完成后时添加跳转到主菜单的逻辑。
打开 RCValues.swift, 在 sharedInstance 属性后添加:
var loadingDoneCallback: (() -> Void)?
var fetchComplete = false
接着,将 fetchCloudValues() 改成:
func fetchCloudValues() {
// WARNING: Don't actually do this in production!
let fetchDuration: TimeInterval = 0
activateDebugMode()
RemoteConfig.remoteConfig().fetch(withExpirationDuration: fetchDuration) { [weak self] status, error in
if let error = error {
print ("Uh-oh. Got an error fetching remote values \(error)")
return
}
RemoteConfig.remoteConfig().activateFetched()
print ("Retrieved values from the cloud!")
let appPrimaryColorString = RemoteConfig.remoteConfig()
.configValue(forKey: "appPrimaryColor")
.stringValue ?? "undefined"
print("Our app's primary color is \(appPrimaryColorString)")
self?.fetchComplete = true
self?.loadingDoneCallback?()
}
}
这里,当请求完成后,将 fetchComplete 设置为 true。最后调用回调函数,通知监听者远程配置值加载完成。这可以用于通知加载屏幕解散自己。
打开 WaitingViewController.swift 添加方法:
func startAppForReal() {
performSegue(withIdentifier: "loadingDoneSegue", sender: self)
}
将 viewDidLoad() 修改为:
override func viewDidLoad() {
super.viewDidLoad()
if RCValues.sharedInstance.fetchComplete {
startAppForReal()
}
RCValues.sharedInstance.loadingDoneCallback = startAppForReal
}
当所有值加载完后,由 RCValue 调用 startAppForReal()。同时还增加了一个判断,防止 RCValue 网络调用有时会在等待屏加载之前完成。这应该不会发生,但这种预防措施无伤大雅。
Todd 编码原则:在代码注释中添加一句“这一点不应该发生”,这将使事情在将来的某个时候真的会发生。
Build & run。你会看到屏幕上出现一个短暂的等待,这取决于你的网速,再跳到其它界面。如果你改变 app 的主颜色值并重新启动应用程序,新颜色将在你的应用程序中正确显示。记得点击在 Firebase 控制台中点击 Publish Changes。
你曾经将一个 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
}
然后,修改 loadDefaultValues():
func loadDefaultValues() {
let appDefaults: [String: Any?] = [
ValueKey.bigLabelColor.rawValue: "#FFFFFF66",
ValueKey.appPrimaryColor.rawValue: "#FBB03B",
ValueKey.navBarBackground.rawValue: "#535E66",
ValueKey.navTintColor.rawValue: "#FBB03B",
ValueKey.detailTitleColor.rawValue: "#FFFFFF",
ValueKey.detailInfoColor.rawValue: "#CCCCCC",
ValueKey.subscribeBannerText.rawValue: "Like Planet Tour?",
ValueKey.subscribeBannerButton.rawValue: "Get our newsletter!",
ValueKey.subscribeVCText.rawValue: "Want more astronomy facts? Sign up for our newsletter!",
ValueKey.subscribeVCButton.rawValue: "Subscribe",
ValueKey.shouldWeIncludePluto.rawValue: false,
ValueKey.experimentGroup.rawValue: "default",
ValueKey.planetImageScaleFactor.rawValue: 0.33
]
RemoteConfig.remoteConfig().setDefaults(appDefaults as? [String: NSObject])
}
添加 3 个工具方法,以检索颜色之外的值:
func bool(forKey key: ValueKey) -> Bool {
return RemoteConfig.remoteConfig()[key.rawValue].boolValue
}
func string(forKey key: ValueKey) -> String {
return RemoteConfig.remoteConfig()[key.rawValue].stringValue ?? ""
}
func double(forKey key: ValueKey) -> Double {
if let numberValue = RemoteConfig.remoteConfig()[key.rawValue].numberValue {
return numberValue.doubleValue
} else {
return 0.0
}
}
然后,将 app 中使用到 AppConstants 的每一部分都替换成对应的 RCValues 调用。。
整个 app 需要有 9 处修改:
打开 ContainerViewController.swift 修改 updateNavigationColors() 方法为:
func updateNavigationColors() {
navigationController?.navigationBar.tintColor = RCValues.sharedInstance.color(forKey: .navTintColor)
}
修改 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)
}
修改 GetNewsletterViewController.swift 的 updateText()方法:
func updateText() {
instructionLabel.text = RCValues.sharedInstance.string(forKey: .subscribeVCText)
submitButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeVCButton), for: .normal)
}
将 PlanetDetailViewController.swift 的 updateLabelColors() 方法的这句:
nextLabel.textColor = AppConstants.detailInfoColor
修改为:
nextLabel.textColor = RCValues.sharedInstance.color(forKey: .detailInfoColor)
将
planetNameLabel.textColor = AppConstants.detailTitleColor
修改为
planetNameLabel.textColor = RCValues.sharedInstance.color(forKey: .detailTitleColor)
将 PlanetsCollectionViewController.swift 中 customizeNavigationBar() 方法的这句:
navBar.barTintColor = AppConstants.navBarBackground
修改为:
navBar.barTintColor = RCValues.sharedInstance.color(forKey: .navBarBackground)
将 collectionView(_:cellForItemAt:) 的这句:
cell.nameLabel.textColor = AppConstants.bigLabelColor
修改为:
cell.nameLabel.textColor = RCValues.sharedInstance.color(forKey: .bigLabelColor)
将 SolarSystem.swift 的 init() 的这句:
if AppConstants.shouldWeIncludePluto {
修改为:
if RCValues.sharedInstance.bool(forKey: .shouldWeIncludePluto) {
将 calculatePlanetScales()的这句:
scaleFactors[i] = pow(ratio, AppConstants.planetImageScaleFactor)
修改为:
scaleFactors[i] = pow(ratio, RCValues.sharedInstance.double(forKey: .planetImageScaleFactor))
嘘,要改的地方太多了,但整个 app 都修改好了。如果你想确认一下,可以搜索 AppConstants —— 你只能搜到一个结果了,那就是结构体自己的定义:
如果想真正确认,删除 AppConstants 文件。app 仍然能通过编译,不会出现报错。
现在你的 app 已经完全封装成远程配置的了,除了将颜色修改为 Greg 非常喜欢的绿色之外,你可以做更多的改变。
打开 Firebase 控制台。进入 Remote Config,点击 Add Parameter。设置 key 为 navBarBackground ,value 设置为 #35AEB1 ,然后点击 Add Parameter。同样,设置 navTintColor 为 #FFFFFF。点击 Publish Changes,将改变推到 app。
做完之后的控制台是这个样子:
发布修改,Build & run.
你的 app 是这样的:
请自己玩一下!修改其它值,随便改下文字。看看什么比较时髦……或者花哨……你可以想出什么样的颜色组合。
当你玩够以后,回到本教程,因为你还有一个严重问题需要处理。
在丹麦,情况紧急!虽然世界上大多数人都认为冥王星不是行星,但北欧保护冥王星协会,一个由狂热的冥王星迷组成的组织,顽固地坚持冥王星作为一颗行星存在,并应当将它放到 Planet Tour App 中。在你阅读本文的同时,哥本哈根的街道上排满了抗议的人群!怎么办?
重新发布一个 App,则会激怒另一群人……
好吧,使用远程配置,这好像不是太难!你可以将 shouldWeIncludePluto 设为 true。等一下,这会改变所有用户的设置,而不仅仅是北欧!怎样才能基于不同的地区下发不同的设置?
答案是 Conditions!
远程配置比起简单的云端字典来说更加智能,那就是根据不同的人群发布不同的设置。你可以利用这个特性允许北欧用户重新接回它们的冥王星。
首先,打开 Firebase 控制台,在 Remote Config 面板中,点击 Add Parameter 添加一个新参数。
key 输入 shouldWeIncludePluto。
点击 value 栏旁边的 Add value for condition 下拉框。选择 Define New Condition。
如果你对 “Experiment with this parameter” 选项感到迷惑,你可以看一下我们的 A/B 测试教程,它刚好延展了这部分内容。
在对话框中,给新条件命名为 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 (或其它北欧国家):
这比飞到哥本哈根要便宜得多了,但同时也少了许多乐趣。
运行程序,这次你可以看见冥王星和别的行星列在一起。呼,避免了一起国际纠纷!
还有另一种不需要修改模拟器设置的方法,在 Xcode 中在 Run 按钮上 Option + 单击。在对话框中,单击 Options 面板,然后在 Application Region 菜单中选择一个合适的国家。
你可以用底部的 Download Material 连接下载最终项目。但是请注意,你仍然需要在 Firabase 控制台中创建项目,并将你的 GoogleServices-info.plist 文件拖到项目中。
有很多特性还没有来得及展示。通过将值下发给随机的用户组,你可以用远程配置来进行 A/B 测试,或者逐步将新功能推广到其他地区。你还可以将不同的数据集下发给通过 Firebase Analytics 识别出的某个用户组,实现某种定制化效果。如果你想进一步了解,请阅读这里以及下一篇教程。
通过远程配置能让你实现许多功能。如果你在开发游戏,如果玩家觉得难度过低或过高,用它来调整游戏玩法是一种好办法。还可以用它来实现“每日提醒”之类的功能。甚至可以用它来实验不同的按钮和标签文本,看看哪种能够让用户体验最好。在你的 App 中试试吧,看看你能改变些什么?
如果你有任何关于 Firebase 远程配置的问题或建议(或者有更好的 Planet Tour 配色方案),请在论坛中留言!
Download Materials