It’s a pretty common scenario to have multiple environments (DEV, TEST, UAT, PROD, etc) in an iOS app. Each environment has different informations related to the app, like tokens, api keys or urls. So, the idea is to find a cool way of handling this multiple values, in an orderly fashion.
When the project starts, we still don’t have several environments, or several different properties between them. Maybe we have just a URL variable that we change manually when we need one or another:
1#define URL @"http://api-dev.com/"
2//#define URL @"http://api-prod.com/"
It’s predictable that when we have more environments, or more properties, this becomes uncontrollable, besides the fact that the developer should remember to change values depending the case.
Another (and still not recommended) approach is to use “conditional compilation“:
2 #define API_TOKEN @"dev_token"
3#define API_URL @"dev_url"
5#define API_TOKEN @"prod_token"
6#define API_URL @"prod_url"
One problem with this approach is that each time we run in a new environment, we should change the configuration to define the macros DEV, PROD, etc. Also, we need to have a global place where we define every configuration property the app needs, and worst of all, they are defined in the cod. Finally, you’re literally compiling different code, for each environment. All this problems grow exponentially, if, besides, you have several environments, where you need to create nested conditions to handle every case.
This is the one. Here we will combine:
- Build Configurations.
- Build Settings.
Switching between environments, without touching the code, we will compile always the same, and all the configurations will be stored in a configuration file (plist).
What is an “Xcode Scheme”? A scheme encapsulates all the bits describing the broader build process for an application. For example: which Target is going to be built or which Configuration should be used to build it (it allows you to select a different configuration for running vs profiling vs archiving). By default, one scheme is created and it’s configured to use Build or Release, depending the action on the scheme (for running, is configured Debug, while for archiving, Release is used).
What we will do is to create one Build Configuration for each environment, set the Build Settings with the appropriate values for each environment (or, better, set a configuration file and set all the info in that file) and, finally, create Schemes to launch each configuration in an easy way.
Step 1: Create Build Configuration
In the project info, under the Configurations section should, exist the default values Debug and Release. Tap the plus (+), sign and duplicate any of the already created Configurations. In the example, we rename Debug and Release and create two more configurations:
Check out that after creating the Configuration, the target Build Settings where updated, to support the new Configurations, for example, the Code Signing Identity, that previously should be configured for Debug and Release, now it’s like this:
Step 2: Create User-Defined Settings
Once we created the Configurations, we need to set the environment values that are different in each of them (tokens, endpoints, etc). To do that, we will create an User-defined Build Settings.
In the project build settings, click the plus sign and select “Add User-Defined Setting”.
New User-Defined settings will appear, so we update the name, and set the appropriate value for each environment.
Settings the user defined variable like $(VariableName) in the Info.plist file, will replace the actual variable during pre compilation, so we can access the plist property in the code.
<strong>Access plist property</strong>
1NSString *endpoint = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"Endpoint"];
2NSString *key = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ApiKey"];
Step 3: Creating Schemes
Now, we already have all the Configurations created and its variables set, we should create a clear way to run each of this Configurations. To do so, we will create one Scheme to launch the app using each of this configurations. We create four environments, so we will create for schemes.
Click on the project current scheme, and then Manage Schemes…
Tap the plus sign and create the four schemes.
Once the schemes are created, we should configure them to launch the appropriate Configuration. So we select the scheme, ant click the “Edit…” button. For each scheme, we need to configure which Configuration should be used for every action (Run, Test, Profile, Analyze and Archive, the Build action doe not have a configuration). Originally, the Debug Configuration was used in the Run, Test and Analyze, while the Release Configuration was used in Profile and Archive, now is up to the user what configuration should be used in each case, I prefer to use the same one in every action for that target.
Another approach, for example, can be creating one scheme for production environment, using UAT Configuration for Run, Test and Analyze and PROD for Profile and Archive, and other schemes for developing, using DEV Configuration for Run, Test and Analyze and TEST for Profile and Archive.
Usually, the schemes are local to each computer, but if we want to be all in the same page, we should check the “Shared” checkbox. This will create an xcshareddata directory in the xcodeproj with the schemes info the is uploaded to the code repository, so every developer has the same schemes.