Aaron Kuehler

80% Scientist, 20% Artist. Theorist and Practitioner

iOS Environments Variables

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:

1
2
3
4
5
6
7
#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, 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:

1
2
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.plist to capture the name of the Build Configuration (environment name) used to create the application bundle at build time:

Configure the Environments

  • Add an Environments.plist to 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.plist for each build configuration name the application supports; in this example the two default build configurations Apple creates for any iOS project: Debug and Release

  • Define the application’s configuration concerns with the appropriate values for each ‘environment’ key in the Environments.plist

Define the Environment object:

1
2
3
4
5
// Environment.h
@interface Environment : NSObject
+ (Environment*) sharedInstance;
- (id) fetch:(NSString*)key;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 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;

1
2
3
4
5
@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:

1
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:

1
MyAppIOS[12351:60b] Using http://www.myapp.com as the API Base URL

Code Example Source