Yeah, me neither.
Fact is, being a successful app developer usually means making frequent changes to your app. Sometimes these changes are new features or bug fixes. But sometimes, the most impactful updates are one-line changes to your code, like adjusting a line of text, or nerfing a powerful unit in a tower defense game.
While these kinds of changes are easy to make, publishing them is still a multi-day process. Wouldn’t it be nice if you could make some of these tweaks without having to go through that whole process?
Firebase Remote Config gives you that power. Throughout the course of this Firebase Remote Config tutorial for iOS, you’ll take the Planet Tour sample app and learn how you can change text, colors, and other behavior on the fly without having to publish new builds! Once mastered, you’ll cover some of the more powerful features, like delivering different sets of content to different users.
Running the Sample App
Get started in this Firebase Remote Config tutorial for iOS by downloading and running the Planet Tour Starter app. You can scroll around to view different planets, tapping on each to get some (mostly-accurate) extra information.
Things at Planet Tour Apps, Inc. are going great, until one day Greg from marketing decides Planet Tour should switch to a green color scheme in celebration of Earth Day. (Yes, you might be doing this Firebase Remote Config tutorial for iOS nowhere near Earth Day, but use your imagination.)
It’s an easy enough fix — if you look in AppConstants.swift there’s an appPrimaryColor
variable you can change, which will affect many of your text label colors. Pushing this change out to your users would involve publishing a new build, submitting it to the App Store, getting it approved, then hoping all of your users download it before Earth Day. To revert the change once Earth Day is over, you’d have to do the whole process over again.
Wouldn’t it be nice if you could just alter these values… from the cloud?
Installing the Remote Config Library
This seems like a good case for Firebase Remote Config. Hop into the nearest available time machine and let’s pretend you decided to wire up your app using Remote Config instead of hard-coded values currently in AppConstants
.
To get started, you’ll need to create a project in the Firebase Console, associate it with the Planet Tour app, then install the Firebase Remote Config library.
Going over this one step at a time:
- Open firebase.google.com/console
- Click on Create New Project.
- Name your project Planet Tour, make sure your region is selected, then click Create Project.
- Next, click on Add Firebase to your iOS app.
- Add the bundle ID of your project (
com.razeware.Planet-Tour
) and leave the App Store ID field blank then click Add App.
- At this point, your browser will download a
GoogleServices-info.plist
file for you. Drag this file into your Xcode project (select Copy Items if Needed). - Click Continue through the remaining few steps in the setup wizard. (Don’t worry — we’ll walk you through those instructions next.)
- Close Planet Tour in Xcode.
- Open a terminal window, navigate to your project and type
pod init
to create a basic Podfile. - Edit your podfile in your favorite text editor so it looks like this:
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
- Run pod install, then open Planet Tour.xcworkspace in Xcode
- Open AppDelegate.swift. Add the following below
import UIKit
:import Firebase
Next, add the following to
application(_:didFinishLaunchingWithOptions:)
above thereturn
statement:FIRApp.configure()
This method looks at what libraries you have installed and initializes them using the constants provided to your project when you added the
GoogleServices-info.plist
file. The Remote Config library now knows the proper place to look on the internet to find new values.
Build and run your app. The app should look as it did previously except, you’ll see some debug information in your console output you didn’t see before.
Congratulations! You’ve installed Remote Config! Now you can go ahead and use it in the rest of this Firebase Remote Config tutorial for iOS.
Using Remote Config
If you want the overly-simplified version of how Remote Config works, think of it as an NSDictionary
living in the cloud. When your app starts up, it grabs any new values it might need from the cloud, and applies them on top of any old values you may have specified as defaults. The general process for using Remote Config looks a little like this:
- Provide Remote Config with defaults for any value you may possibly change in the future.
- Fetch any new values from the cloud. These are kept in a cached holding pattern on your device.
- “Activate” those fetched values. When this happens, you can think of this as applying those fetched values on top of your existing default values.
- Query Remote Config for values. Remote Config will either give you a value from the cloud (if it found one), or a default value based on the provided key.
One important thing to note is these new values you fetch are generally a subset of the default values you supply. You can take nearly any hard-coded string, number, or boolean in your app, and wire them up to use Remote Config. This gives you the flexibility to change many aspects of your app later, while still keeping your actual network calls nice and small.
Enough theory. Time to put this into practice!
First, open your Utilities folder in the Planet Tour Xcode project and right-click to create a new file. Select Swift file. Name it RCValues.swift, and create it in the default folder suggested by Xcode.
Replace the contents of the file with the following:
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) } } |
Here you use the Singleton approach for the RCValues
class. Inside loadDefaultValues()
, you’re passing along a set of keys and values to the Remote Config as defaults. Right now, you’re only suppling one value, but don’t worry. You’ll add more later.
Next, you need to ask Remote Config to fetch new values from the cloud. Add the following to the end of init()
:
fetchCloudValues() |
Next, add the following method below loadDefaultValues()
to fetch these values:
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!") } } |
Let’s go over what you did here:
1. By default, Remote Config will cache any values it retrieves from the cloud for about 12 hours. In a production app, this is probably just fine. But when you’re doing development (or following a Firebase Remote Config tutorial for iOS online), this makes it really tough to test out new values. So instead, you’re specifying a fetchDuration
of 0
to ensure you never use the cached data.
2. In the completion handler, you activate these fetched values immediately — i.e., you’re telling remote config to apply these newer values on top of any older ones it might have.
Now, the code you added at the beginning has some issues. The Remote Config library has a client-side throttle to ensure you don’t ping the service too frequently. By setting your fetchDuration
to 0
, you’ll hit this throttle and your library will stop making calls.
You can get around this by enabling developer mode. Add the following method below fetchCloudValues()
:
func activateDebugMode() { let debugSettings = FIRRemoteConfigSettings(developerModeEnabled: true) FIRRemoteConfig.remoteConfig().configSettings = debugSettings! } |
By setting developer mode to true
, you’re telling Remote Config to bypass the client-side throttle. For development purposes, or testing with your 10-person team, this is fine. But if you were to launch this app to the public with your millions of adoring fans, you’re going to hit the server-side throttle pretty quickly, and Remote Config will stop working. (This is the whole reason you have a client-side throttle in the first place.)
So, for crying out loud, before you launch this app for real, make sure you disable developer mode and set your fetchDuration
to something a little more reasonable, like 43200
(that’s 12 hours to you and me).
Finally, add the following right after you set fetchDuration
:
activateDebugMode() |
The above will activate debug mode and you will no longer hit the server-side throttling issue.
Okay, time to get this code up and running. Open AppDelegate.swift, add the following to application(_:didFinishLaunchingWithOptions:)
, below FIRApp.configure()
:
let _ = RCValues.sharedInstance |
Build and run. You should see the following line in your debug console:
Retrieved values from the cloud! |
Using Remote Config Values
Now that you’re downloading these values, try printing them to the console. Add the following to your fetchCloudValues()
call, right after the “Retrieved values from the cloud” line:
print ("Our app's primary color is \(FIRRemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor"))") |
This will grab the appropriate value for your appPrimaryColor
key.
Build and run your app. You’ll see a line like this:
Our app's primary color is <FIRRemoteConfigValue: 0x61000003ece0> |
Well, that’s somewhat helpful, but you’re kind of hoping for a string value.
Remote Config retrieves values as FIRRemoteConfigValue
objects, which you can think of as wrappers around the underlying data, which is represented internally as a UTF8-encoded string). You’ll almost never use this object directly. Instead, you’ll call a helper method like numberValue
or boolValue
to retrieve the actual value you want.
Replace the line you just added with:
print ("Our app's primary color is \(FIRRemoteConfig.remoteConfig().configValue(forKey: "appPrimaryColor").stringValue)") |
Build and run your app; this time you’ll see:
Our app's primary color is Optional("#FBB03B") |
That’s more like it. Remote Config provides you the default value you’ve supplied earlier.
Updating Values from the Cloud
Now you’re getting proper values from Remote Config, let’s try supplying new values from the cloud.
Open the Firebase Console and click on the Remote Config header on the left. Click Add your first parameter. In the form, enter appPrimaryColor for the key and Greg from Marketing’s favorite new green — #36C278 — for the value.
Click Add Parameter, then click Publish Changes to update the changes.
Build and run your app. Take a look at what you have in the console now….
Our app’s primary color is Optional("#36C278") |
Hooray! You’re updating values from the cloud!
Changing Your App’s Look and Feel
While this new Xcode console output might be exciting to you and me, your audience will be considerably less excited. Lets hook up your app to use this new value.
First, add an enum to represent your keys. Using raw strings for key names is a recipe for disaster — or at least spending an afternoon hunting down a mystery bug because you mistyped a key name. By using an enum, Xcode can catch errors at compile time instead of runtime.
Open RCValues.swift and add the following above the class definition:
enum ValueKey: String { case appPrimaryColor } |
Next, update loadDefaultValues()
to use this enum instead of the raw string:
let appDefaults: [String: NSObject] = [ ValueKey.appPrimaryColor.rawValue : "#FBB03B" as NSObject ] |
Next, add the following helper method to RCValues
, which takes in a ValueKey
and returns a UIColor
based on the string from Remote Config.
func color(forKey key: ValueKey) -> UIColor { let colorAsHexString = FIRRemoteConfig.remoteConfig()[key.rawValue].stringValue ?? "#FFFFFFFF" let convertedColor = UIColor(rgba: colorAsHexString) return convertedColor } |
Finally, change the places in your app using the old AppConstants
value to use this new RCValues
helper method instead. You’ll do this in three locations.
1. Open ContainerViewController.swift, change the following inside updateBanner()
:
bannerView.backgroundColor = AppConstants.appPrimaryColor |
to this:
bannerView.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor) |
2. Open GetNewsletterViewController.swift, change the following inside updateSubmitButton()
:
submitButton.backgroundColor = AppConstants.appPrimaryColor |
to this:
submitButton.backgroundColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor) |
3. Open PlanetDetailViewController.swift, change the following inside updateLabelColors()
:
nextLabel.textColor = AppConstants.appPrimaryColor |
to this:
nextLabel.textColor = RCValues.sharedInstance.color(forKey: .appPrimaryColor) |
To be thorough, open AppConstants.swift and delete the following:
static let appPrimaryColor = UIColor(rgba: "#36C278") |
See ya later, hard-coded value…
Build and run your app. You should see the new green throughout your app.
You don’t have a lot of control as to when these new values get applied. First time you ran the app, you probably saw the default orange on the main menu then the new green on the planet detail screens once your new values were loaded from the cloud.
This can be a bit confusing to your users. Sure, in this case, you’re only changing some label colors, but if your app starts changing text or values affecting its behavior while your user is in the middle of using it, it can be quite confusing.
There are numerous ways you can deal with this issue, but perhaps the easiest might be to create a loading screen. As luck would have it, there’s already one partially set up for you.
Hooking Up a Loading Screen
First thing you’ll want to do is make the loading screen your app’s initial view controller. Open Main.storyboard and Control-drag from your Navigation Controller to the Waiting View Controller — it’s the view controller with the black background, although it might be easier to do this Control-dragging in your storyboard outline. Select root view controller from the pop-up: this will make your loading screen the initial screen when your app loads.
Now, you’ll add the logic to transition to the main menu when Remote Config is finished loading.
Open RCValues.swift, add the following below your sharedInstance
property:
var loadingDoneCallback: (() -> ())? var fetchComplete: Bool = false |
Next, add the following two lines indicated below, to the end of your completion handler in fetchCloudValues()
func fetchCloudValues() { // WARNING: Don't actually do this in production! 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)") // ADD THESE TWO LINES HERE! self?.fetchComplete = true self?.loadingDoneCallback?() } } |
Here, you set the fetchComplete
variable to true indicating fetching is complete. Finally you call the optional callback to inform the listener the Remote Config values have finished loading. This could be used to tell a loading screen to dismiss itself.
Open WaitingViewController.swift and add the following method:
func startAppForReal() { performSegue(withIdentifier: "loadingDoneSegue", sender: self) } |
Next, replace viewDidLoad()
with the following:
override func viewDidLoad() { super.viewDidLoad() if RCValues.sharedInstance.fetchComplete { startAppForReal() } RCValues.sharedInstance.loadingDoneCallback = startAppForReal } |
Here, you’re making startAppForReal()
the method RCValues
calls when all of its values are done loading. You’re also adding a check just in case RCValues
somehow manages to finish its network call before the waiting screen is done loading. This should never happen, but it never hurts to code defensively!
Build and run your app. You’ll see the waiting screen appear for a short while (depending on your network speed) before jumping into the rest of your app. If you change the value of your app’s primary color and restart your app, the color will properly appear everywhere in your app.
Hook Up the Rest of Your App
Now you’ve converted one value from AppConstants
to RCValues
, you can convert the rest! Open RCValues.swift and replace ValueKey
with the following:
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 } |
Next, replace loadDefaultValues()
with the following:
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) } |
Next, add three helper methods to allow retrieving values other than colors:
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 } } |
Next, replace every part of your app that uses AppConstants
with the corresponding call to RCValues
. You’ll make nine changes throughout your app:
1. Open ContainerViewController.swift, replace updateNavigationColors()
with the following:
func updateNavigationColors() { navigationController?.navigationBar.tintColor = RCValues.sharedInstance.color(forKey: .navTintColor) } |
2. Replace updateBanner()
with the following:
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. Open GetNewsletterViewController.swift, replace updateText()
with the following:
func updateText() { instructionLabel.text = RCValues.sharedInstance.string(forKey: .subscribeVCText) submitButton.setTitle(RCValues.sharedInstance.string(forKey: .subscribeVCButton), for: .normal) } |
4. Open PlanetDetailViewController.swift, in updateLabelColors()
replace the line
nextLabel.textColor = AppConstants.detailInfoColor |
with
nextLabel.textColor = RCValues.sharedInstance.color(forKey: .detailInfoColor) |
5. Replace the line
planetNameLabel.textColor = AppConstants.detailTitleColor |
with
planetNameLabel.textColor = RCValues.sharedInstance.color(forKey: .detailTitleColor) |
6. Open PlanetsCollectionViewController.swift, and inside customizeNavigationBar()
replace the line
navBar.barTintColor = AppConstants.navBarBackground |
with
navBar.barTintColor = RCValues.sharedInstance.color(forKey: .navBarBackground) |
7. Inside collectionView(_:cellForItemAt:)
, replace the line
cell.nameLabel.textColor = AppConstants.bigLabelColor |
with
cell.nameLabel.textColor = RCValues.sharedInstance.color(forKey: .bigLabelColor) |
8. Open SolarSystem.swift, inside init()
, replace the line
if AppConstants.shouldWeIncludePluto { |
with
if RCValues.sharedInstance.bool(forKey: .shouldWeIncludePluto) { |
9. Finally, inside calculatePlanetScales()
replace the line
scaleFactors[i] = pow(ratio, AppConstants.planetImageScaleFactor) |
with the line
scaleFactors[i] = pow(ratio, RCValues.sharedInstance.double(forKey: .planetImageScaleFactor)) |
Whew! Okay, that was a lot of changes, but now you should have your entire app switched over. If you want to be sure, do a search in your app for “AppConstants” — you should only have one result left, which is defining the struct itself.
If you want to be really sure, delete your AppConstants
file. Your app should continue to build and run without any errors.
Now your app is fully wired up to Remote Config, you can make some other changes to go along with the green color that Gary likes so much.
Open the Firebase Console. Make sure you’re in the Remote Config section, then click Add Parameter. Choose navBarBackground for the key, and #35AEB1 for the new value, then click Add Parameter. Then do the same thing to set navTintColor to #FFFFFF. Click Publish Changes to publish these changes to your app.
When you’re finished, your console should look like this:
Your app should look a little something like this:
Feel free to play around! Change some other values. Mess around with the text. See what kind of stylish… or gaudy… color combinations you can come up with.
But when you’re finished, come on back to the Firebase Remote Config tutorial for iOS because you have an international crisis to deal with!
Bringing Back Pluto
Things are rotten in the state of Denmark! While most of the world has begrudgingly accepted Pluto isn’t a planet, the Scandinavian Society for Preserving Pluto, a totally-not-made-up society of rabid Pluto fans, has lobbied hard for Pluto to be a planet and hence worthy of inclusion in Planet Tour. As you read this, protests are mounting in the streets of Copenhagen! Whatever can be done?
Well, this seems like a simple job for Remote Config! You could set shouldWeIncludePluto
to true
. But hang on; that will change this setting for all of your users, not just those in Scandinavia. How can you deliver different settings just to residents of these countries?
Conditions to the Rescue!
What makes Remote Config more sophisticated than just a simple dictionary in the cloud is its ability to deliver different sets of data to different people. You’ll take advantage of this feature in order to make Pluto a planet again for your Scandinavian users.
First, open the Firebase Console, make sure you’re in the Remote Config panel, and click Add Parameter to add a new parameter
Enter shouldWeIncludePluto as the parameter key.
Next, click the dropdown next to the value field labelled Add value for condition.
Next, select Define New Condition.
In the dialog, give the new condition a name of Pluto Fans.
In the drop-down underneath, select Device Region / Country.
From the countries list, select Denmark, Sweden, Norway, Iceland, and Finland.
Click Create Condition.
Next, add a value of true in the Value for Pluto Fans field, and a value of false as your default value.
Finally, click Add Parameter, then click Publish Changes to push these changes to the world.
Build and run your app. Assuming you don’t live in one of these northernly countries, you still probably won’t see Pluto in your list of planets. If you want to test the experience for your Scandinavian users, I recommend booking a flight to Copenhagen, getting yourself a new Danish iPhone, then running your app on it, perhaps while enjoying a smoked-salmon open-faced sandwich.
A slightly more economical option (potentially involving less jet lag) is to open the Settings app on your device or simulator. Select General > Language & Region > Region > Denmark (or whatever your favorite Scandinavian country is).
Build and run your app; this time you should see Pluto back where it belongs, among the other planets. Whew! International crisis averted!
Where to Go From Here?
You can download the fully finished project for this Firebase Remote Config tutorial for iOS here. Please note, however, you still need to create a project in the Firebase Console and drag in your GoogleServices-info.plist
file for the project to work.
There’s a lot you can do with Remote Config. If you’re developing any kind of game, it can be a great way to tweak your gameplay if your players are finding it too easy or too hard. It’s also an easy way to create a “Message of the Day” kind of feature. Or just use it to experiment with different button or label text to see what your users react to best. Try it out in your favorite app and see what you can change on the fly!
There are more features you haven’t touched on yet, too. By delivering values to random groups of users, you can use Remote Config to run A/B tests, or gradually roll out new features. You can also deliver different data sets to specific groups of people you’ve identified in Firebase Analytics, which gives you some nice customization features. If you want to find out more, you can check out the documentation.
As always, if you have any questions or comments about this Firebase Remote Config tutorial for iOS (or Planet Tour color schemes) please join the forum discussion below!
The post Firebase Remote Config Tutorial for iOS appeared first on Ray Wenderlich.