记得发布 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 远程配置库。
让我们一步步来:
-
点击创建新项目。
-
将项目命名为 Plannet Tour,确保选择你所在的地区,然后点击创建项目。
[图片上传失败...(image-bcf3fe-1558441280353)]
-
然后,点击 Add Firebase to your iOS app。
image -
填入你的 Bundle Id(com.razeware.Planet-Tour) ,App Store ID 一栏保留为空。然后点 Add App。
image -
这时,浏览器会下载 GoogleServices-info.plist 文件。将这个文件拖到 Xcode 项目中(选中 Copy Items if Needed)。
-
剩下来的步骤就是在设置向导中点击 Continue。(别担心,后面我们会带你完成这个步骤)。
-
在 Xcode 中关闭 Planet Tour 项目。
-
打开终端,进入项目目录,输入 pod init 创建一个基本的 Podfile 文件。
-
编辑 Podfile :
target 'Planet Tour' do
# Comment this 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 in Xcode。
-
打开 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 启动时,它会从云端获取新值,并在应用你默认指定的旧值之前优先使用它们。远程配置的正常工作流程如下:
- 以 defaults 形式将你有可能在未来改变的值提供给远程配。
- 从云端抓取新值。这些会保存到设备的缓存中。
- “激活”这些值。你可以看成,抓取到的值被应用到原来的 defaults 值上。
- 向远程配置索要值。远程配置要么给你从云端获取到的值(如果有的话),要么给你默认值。
]
值得注意的一点是,从云端抓取的新值通常是你指定的默认值的子集。你可以使用 App 中任何硬编码的字符串、数字或布尔值,并将它们用远程配置连接起来。这种方式很灵活,你可以在未来改变 App 的任何一个方面,但保持你的网络接口整洁、灵巧。
课本念完了。接下来要进行实际操作。
首先,在 Xcode 中打开 Utilities 文件夹,用右键创建新文件。选择 Swift 文件,就叫做 RCValues.swift,将它保存在 Xcode 默认的文件夹。
编辑它的内容为:
import Foundation
import Firebase
class 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!")
}
}
代码解释如下:
- 默认,远程配置会缓存从云端获取的值大约 12 个小时。在真实 App 中,这可能够了。但对于你现在要干的事情来说(或者后续一个远程配置教程),这就麻烦大了。因此,你在这里指定了 fetchDuration 为 0,这样永远不会采用缓存数据。
- 在完成闭包中,你立即激活这些新抓取到的值,例如,你告诉远程配置,如果一旦发现有新值,立即覆盖旧值。
现在,你在一开始加入的代码出现一点问题。远程配置库有一个客户端的限制,避免你频繁地 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 之类的助手方法转为你想要的类型。
[图片上传失败...(image-265cc6-1558441280357)]
将刚刚添加的这行为:
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。
[图片上传失败...(image-46f9c6-1558441280357)]
点 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 助手方法替代。总共有三个地方:
-
打开 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 删掉这句:
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 启动时的第一个画面。
image现在,来加一些代码使得当远程配置完成抓取后跳转到主菜单页面。
打开 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() 替换为:
```swift
override 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 值。你可以这样:
-
打开 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 { 替换为: ```swift if RCValues.sharedInstance.bool(forKey: .shouldWeIncludePluto) {
-
最后, 在 calculatePlanetScales() 中将这句:
scaleFactors[i] = pow(ratio, AppConstants.planetImageScaleFactor)
替换为:
scaleFactors[i] = pow(ratio, RCValues.sharedInstance.double(forKey: .planetImageScaleFactor))
嘘! 好了,改完这么多地方,你可以来确认一下你的 App 是否都改完了。这样,你可以搜索 App 中的所有 “AppConstants” — 你只会找到一个结果,那就是它自己的结构定义。
image如果你还不信,你可以把 AppConstants 文件删除。你的 App 依然能够编译,没有任何问题。
现在,你的 App 已经完全支持远程配置了,你可以继续实现 Gary 要求你做的任何其它改变。
打开 Firebase 控制台。在 Remote Config 节点击 Add Parameter。key 输入 navBarBackground ,value 输入 #35AEB1,点击 Add Parameter。重复重样动作将 navTintColor 设为 #FFFFFF。点击 Publish Changes ,将值下发给 App。
你的 Firebase 控制台最终将是这个样子:
image
App 则是这个样子:
image你可以任意调整,试试修改其它值。胡乱改几个文字内容。尝试各种样式……花色……颜色。
但当你做完这一切之后,请回到本教程,这里还有一个严重的问题有待解决!
冥王星回归
事情就坏在丹麦人身上!当世界上大部分人都已经接受冥王星不再是一颗行星的同时,北欧保护冥王星协会,一个由狂热的冥王星迷组成的组织,顽固地坚持冥王星作为一颗行星存在,并应当将它放到 Planet Tour App 中。在你阅读本文的同时,哥本哈根的街道上排满了抗议的人群!怎么办?
重新发布一个 App,则会激怒另一群人……
好吧,使用远程配置,这好像不是太难!你可以将 shouldWeIncludePluto 设为 true。等等,这回改变所有用户的设置,而不仅仅是北欧。怎样才能基于不同的地区下发不同的设置?
答案是 Conditions!
远程配置比起简单的云端字典来说更加智能,那就是根据不同的人群发布不同的设置。你可以利用这个特性允许北欧用户重新迎回它们的冥王星。
首先,打开 Firebase 控制台,在 Remote Config 面板中,点击 Add Parameter 添加一个新参数。
key 输入 shouldWeIncludePluto。
点击 value 栏旁边的 Add value for condition 下拉框。选择 Define New Condition。
image
在对话框中,给新条件命名为 Pluto Fans。
在下拉框中,选择 Device Region / Country。
在国家列表中,选择 Denmark, Sweden, Norway, Iceland, 和 Finland。
[图片上传失败...(image-206654-1558441280356)]
点击 Create Condition。
然后,在 Value for Pluto Fans 栏,输入值 true。在 Default value 栏输入 false。
image最后,点击 Add Parameter,再点击 Publish Changes。
运行程序,假设你没有在这些北半球国家,你仍然不能在行星列表中看见冥王星。如果你想体验一下北欧用户,我建议你买一张到哥本哈根的机票,买一部丹麦版的 iPhone,然后打开 App,顺便来一块熏鲑鱼单片三明治。
有一个更经济的做法(同时更少的时差)是,打开设备后模拟器上的设置程序。选择 General > Language & Region > Region > Denmark (或其它北欧国家)
image这比飞到哥本哈根要便宜得多了,但同时也少了许多乐趣。
运行程序,这次你可以看见冥王星和别的行星列在一起。呼,避免了一起国际纠纷!
image结束语
你可以从这里下载最终项目。但是请注意,你仍然需要在 Firabase 控制台中创建项目,并将你的 GoogleServices-info.plist 文件拖到项目中。
通过远程配置能让你实现许多功能。如果你在开发游戏,如果玩家觉得难度过低或过高,用它来调整游戏玩法是一种好办法。还可以用它来实现“每日提醒”之类的功能。甚至可以用它来实验不同的按钮和标签文本,看看哪种能够让用户体验最好。在你的 App 中试试吧,看看你能改变些什么?
有很多特性还没有来得及展示。通过将值下发给随机的用户组,你可以用远程配置来进行 A/B 测试,或者逐步将新功能推广到其他地区。你还可以将不同的数据集下发给通过 Firebase Analytics 识别出的某个用户组,实现某种定制化效果。如果你想进一步了解,请阅读这个文档。
如果你有任何关于 Firebase 远程配置(或者 Planet Tour 颜色方案)的问题或建议,请在下面留言!
网友评论