A brand new Xcode project defines two build configurations, Debug and Release. Most projects define one or more additional build configurations for various reasons. This isn't new and it's a good practice to use build configurations to tailor a build to the specific needs of the environment it's going to be deployed to.
In this episode, I show you how to safely manage data that is specific for a build configuration, such as API keys, credentials, and other sensitive data. There are several strategies to manage build configurations, but there are important differences you need to consider, especially in the context of security.
Adding a Configuration
Fire up Xcode and create a new project by choosing the Single View App template from the iOS > Application section.
Name the project Configurations, set Language to Swift, and make sure the checkboxes at the bottom are unchecked. Tell Xcode where you'd like to store the project and click Create.
Open the Project Navigator on the left and click the project at the top. Select the project in the Project section to show the project details. A brand new Xcode project defines two build configurations, Debug and Release.
You commonly use the Debug configuration during development whereas the Release configuration is used for creating App Store or TestFlight builds. This probably isn't new to you.
Many applications communicate with a backend and it's a common practice to have a staging and a production environment. The staging environment is used during development. Double-click the Debug configuration and rename it to Staging.
Let's add a third configuration for the production environment. Click the + button at the bottom of the table, choose Duplicate "Staging" Configuration, and name the configuration Production.
Let's create a scheme for each configuration to make it easy to quickly switch between environments. Select the scheme at the top and choose Manage Schemes... from the menu. Select the scheme named Configurations and click it one more time. Rename it to Staging.
With the scheme selected, click the gear icon at the bottom and choose Duplicate. Name the scheme Production. Select Run on the left and set Build Configuration to Production.
That's it. We now have a build configuration for staging and production. The schemes make it quick and easy to switch between build configurations.
User-Defined Build Settings
As I mentioned earlier, there are several solutions to manage data that is specific to a particular build configuration. In this episode, I show a solution I use in any project that has some complexity to it. Before I lay out the solution I have in mind, I'd like to show you another solution that is often used by developers.
Choose the project in the Project Navigator on the left. Select the Configurations target from the Targets section and click the Build Settings tab at the top.
The Build Settings tab shows the build settings for the Configurations target. It's possible to expand this list with build settings that you define. Click the + button at the top and choose Add User-Defined Setting.
Name the user-defined setting BASE_URL. Defining a base URL is common for applications that interact with a backend.
What is the benefit of defining a user-defined setting? The user-defined setting allows us to set a value for BASE_URL for each build configuration. Click the triangle on the left of the user-defined setting to show the list of build configurations.
Set the value for Production and Release to https://cocoacasts.com and the value for Staging to https://staging.cocoacasts.com.
As the name implies, a build setting is available during the build process. Your code cannot directly access the build settings you define. That is a common misconception. There's a solution, though. Open Info.plist and add a new key/value pair. Set the key to BASE_URL and the value to $(BASE_URL)
.
What is the value of adding a key/value pair to the project's Info.plist? The value of the key/value pair is updated at build time. Xcode inspects the build settings for the current build configuration and sets the value of the BASE_URL key in project's Info.plist.
Let's try it out. Open AppDelegate.swift and navigate to the application(_:didFinishLaunchingWithOptions:)
method. We need to access the contents of the Info.plist file. This is possible through the infoDictionary
property of the Bundle
class. The bundle we are interested in is the main bundle. As the name implies, the infoDictionary
property is a dictionary of key/value pairs and we access the value for the BASE_URL
key.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(Bundle.main.infoDictionary?["BASE_URL"])
return true
}
Select the Production scheme at the top and run the application in the simulator. Inspect the output in the console.
Optional(https://staging.cocoacasts.com)
Select the Staging scheme at the top and run the application in the simulator. Inspect the output in the console. That's pretty nice. Right?
Optional(https://staging.cocoacasts.com)
A Word About Security
Most developers find this solution very convenient. And it is. There's one problem, though. It's easy to extract the Info.plist file from applications downloaded from the App Store. What happens if you store API keys, credentials, or other sensitive information in the Info.plist file? This introduces a significant security risk that we can and should avoid.
I'd like to show you a simple solution that offers the same flexibility, type safety, and improved security. Create a new Swift file and name it Configuration.swift.
Define an enum with name Configuration and a raw value of type String
. We define a case for each build configuration, staging
, production
, and release
.
import Foundation
enum Configuration: String {
// MARK: - Configurations
case staging
case production
case release
}
Before we continue with the implementation of the Configuration enum, we need to update the Info.plist file. Open Info.plist and add a key/value pair. The key is Configuration and the value is $(CONFIGURATION)
. The value of CONFIGURATION is automatically set for us. We don't need to worry about it. As the name implies, the value is equal to the name of the build configuration with which the build is created.
Revisit Configuration.swift. We want easy access to the configuration of the build, the value stored in the project's Info.plist file. Define a static, constant property, current
, of type Configuration
. We access the value that is stored in the Info.plist file for the key Configuration and cast it to a String
instance. We throw a fatal error if this fails because that should never happen.
import Foundation
enum Configuration: String {
// MARK: - Configurations
case staging
case production
case release
// MARK: - Current Configuration
static let current: Configuration = {
guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
fatalError("No Configuration Found")
}
}()
}
We use the value from the Info.plist file to create a Configuration
instance. We throw another fatal error if the initialization fails. Notice that we lowercase the value stored in the Info.plist file. The Configuration
instance is returned from the closure. Don't forget to append a pair of parentheses to the closure.
import Foundation
enum Configuration: String {
// MARK: - Configurations
case staging
case production
case release
// MARK: - Current Configuration
static let current: Configuration = {
guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
fatalError("No Configuration Found")
}
guard let configuration = Configuration(rawValue: rawValue.lowercased()) else {
fatalError("Invalid Configuration")
}
return configuration
}()
}
Let's try it out. Open AppDelegate.swift and print the current configuration. Select the Staging scheme, run the application, and inspect the output in the console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(Configuration.current)
return true
}
staging
Extending Configuration
You probably know that I like to leverage enums for creating namespaces. Let me show you how we can improve the current implementation of the Configuration
enum. The simplest solution is to define a static, computed property, baseURL
, of type URL
. You can also define the property as a static, constant property. That's a personal choice. We use a switch
statement to return the base URL for each build configuration.
import Foundation
enum Configuration: String {
// MARK: - Configurations
case staging
case production
case release
// MARK: - Current Configuration
static let current: Configuration = {
guard let rawValue = Bundle.main.infoDictionary?["Configuration"] as? String else {
fatalError("No Configuration Found")
}
guard let configuration = Configuration(rawValue: rawValue.lowercased()) else {
fatalError("Invalid Configuration")
}
return configuration
}()
// MARK: - Base URL
static var baseURL: URL {
switch current {
case .staging:
return URL(string: "https://staging.cocoacasts.com")!
case .production, .release:
return URL(string: "https://cocoacasts.com")!
}
}
}
There are several details I'd like to point out. First, I don't use a default
case. I'm explicit about the value that is returned for each build configuration. This makes it easier to spot problems and it makes the code more readable and intuitive. Second, I use the exclamation mark to force unwrap the value returned by the initializer of the URL
struct. This is one of the rare scenarios in which I use the exclamation mark to force unwrap a value. It is convenient, but, more importantly, the base URL should never be equal to nil
.
Open AppDelegate.swift and print the value of the baseURL
computed property. With the scheme set to the Staging, run the application and inspect the output in the console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(Configuration.baseURL)
return true
}
https://staging.cocoacasts.com
This pattern has a few advantages over the first solution we implemented. We take advantage of Swift's type safety and we're no longer dealing with an optional value. We can also benefit from Xcode's autocompletion. These advantages are subtle, but you come to appreciate them over time.
Let's put the cherry on the cake by adding namespaces to the mix. Create a new Swift file and name it Configuration+DarkSky.swift.
Create an extension for the Configuration enum and define an enum with name DarkSky
in the extension. Dark Sky is a weather service I use from time to time.
import Foundation
extension Configuration {
enum DarkSky {
}
}
The DarkSky
enum defines a static, constant property, apiKey
, of type String
. We switch on the current configuration and return a different value for each build configuration. As I mentioned earlier, you can also declare the property as a static, variable property. That is up to you to decide.
import Foundation
extension Configuration {
enum DarkSky {
static let apiKey: String = {
switch Configuration.current {
case .staging:
return "123"
case .production:
return "456"
case .release:
return "789"
}
}()
}
}
There are several advantages to this approach. The configuration for the Dark Sky API is nicely namespaced. This also makes it easy to put the configuration for the Dark Sky API in a separate file.
Open AppDelegate.swift and print the API key for the Dark Sky weather service. With the scheme set to Staging, run the application and inspect the output in the console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print(Configuration.DarkSky.apiKey)
return true
}
123
What's Next?
This pattern adds convenience, type safety, and security to your projects. It's easy to adopt and, once it's implemented, it's straightforward to extend. It's a pattern I enjoy using for a range of reasons. I strongly recommend to give it a try.