iOS Environment Variables
In short: I show a cleaner way to handle the settings an iOS app needs – such as which web address it should talk to – by moving them out of the code and into a separate file that lists the right values for each build. At startup the app checks how it was built and loads the matching settings, so I can change configuration without touching or breaking the app’s code.
I find that there are usually at least two distinct environments in which any iOS project is built: generally one set of configuration for development and another for production. Each mode usually requires its own set of configuration: resource URLs for data fetching and manipulation, 3rd party service authentication keys, and the like. Usually I see these types of configurations defined through conditional macros inline with application code, like so:
#ifdef DEBUG
const NSString* myAppAPIBaseUrl = @"http://development.myapp.com"; // development services
#else
const NSString* myAppAPIBaseUrl = @"http://www.myapp.com"; // production services
#endif
[[MyAppAPIClient alloc] initWithBaseUrl:myAppAPIBaseUrl];
In this approach, the URL used to initialize the MyAppAPIClient is determined by the truth-y presence of the DEBUG preprocessor macro at build time, by the truth-y presence of the DEBUG preprocessor macro at build time, by the truth-y presence of the DEBUG preprocessor macro at build time, implicitly describing two separate configurations in which the application is built– presumably DEBUG (development) and otherwise (production). This ad-hoc, imperative approach is generally scattered across the code base; choosing to co-locate configuration and the using class(es). Maintaining values in this manner proves to be challenging over time; configuration requirements change, new application components share configuration values essentially making the existence of one class implementation dependent upon another solely for its constant definitions.
What if the application code and configuration could live independent of each other? This would imply that configuration changes are less likely to introduce regressions into the application code. For example, imagine the above example rewritten to simply ask for the value of myAppAPIBaseUrl:
NSString* myAppAPIBaseUrl = [[Environment sharedInstance] fetch:@"MyAppAPIBaseUrl"];
[MyAppAPIClient alloc] initWithBaseUrl:myAppAPIBaseUrl];
It is the responsibility of the [[Environment sharedInstance] fetch:@"MyAppAPIBaseUrl"] message to determine the appropriate value to return based on configuration living apart from the application code.
As it turns out, implementing this behavior is not at all complicated:
Capture the build configuration used to build the Application bundle
- Add a new String value to the
<application_name>-Info.plistto capture the name of the Build Configuration (environment name) used to create the application bundle at build time:

Figure 1: application-info.plist
Configure the Environments
Add an
Environments.plistto the application. This serves as the centralized listing of environment specific configuration values for each environment the application supports.Add a Dictionary type entry to the
Environments.plistfor each build configuration name the application supports; in this example the two default build configurations Apple creates for any iOS project:DebugandReleaseDefine the application’s configuration concerns with the appropriate values for each ’environment’ key in the
Environments.plist

Figure 2: environments.plist
Define the Environment object:
// Environment.h
@interface Environment : NSObject
+ (Environment*) sharedInstance;
- (id) fetch:(NSString*)key;
@end
// Environment.m
#import "Environment.h"
static Environment *sharedInstance = nil;
@interface Environment()
@property (strong, nonatomic) NSDictionary* environment;
@end
@implementation Environment
+ (Environment*) sharedInstance {
static Environment *_sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedInstance = [[Environment alloc] init];
});
return _sharedInstance;
}
- (id)init
{
self = [super init];
if (self) {
NSBundle* bundle = [NSBundle mainBundle];
// Read in the 'Environment' name used to build the application (Debug or Release)
NSString* configuration = [bundle objectForInfoDictionaryKey:@"Configuration"];
// Load the Environment.plist
NSString* environmentsPListPath = [bundle pathForResource:@"Environments" ofType:@"plist"];
NSDictionary* environments = [[NSDictionary alloc] initWithContentsOfFile:environmentsPListPath];
// Read the values for the 'Environment' name into the 'environment property'
NSDictionary* environment = [environments objectForKey:configuration];
self.environment = environment;
}
return self;
}
- (id)fetch:(NSString*)key {
/**
* If the key is present in the environment, then return its value;
* otherwise return nil.
*/
return [self.environment objectForKey:key];
}
@end
Example
With the above code in place we can run the application to make sure everything’s wired up correctly. In my example, I’ve added the following logger statement to the application’s launch lifecycle;
@implementation MAIOSAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
NSLog(@"Using %@ as the API Base URL", [[Environment sharedInstance] fetch:@"MyAppAPIBaseUrl"]);
...
@end
When the application is built in the Development mode, with the Debug build configuration, the log statement outputs:
MyAppIOS[12289:60b] Using http://development.myapp.com as the API Base URL
And when using the Production mode, with the Release build configuration, the log statement outputs:
MyAppIOS[12351:60b] Using http://www.myapp.com as the API Base URL
Example Application Source Code
Glossary
- environment
- a distinct setup an app runs in, such as development (for testing) versus production (for real users).
- build configuration
- a named set of build settings, such as Debug or Release, that Xcode uses to compile the app.
- application bundle
- the packaged folder holding the finished app and the resource files it ships with.
- preprocessor macro
- a named flag the compiler swaps in before building, used here to pick values by build type.
- property list (plist)
- an Apple file format that stores settings as a list of keys and values.
- regressions
- new bugs that break behaviour which used to work.