YAML-based Configuration for ObjC Projects
Published on
Probably every iOS or OS X app has to deal with configurations such as server address, analytics service API key and so on. Usually these configurations are different for development and production environment, e.g.: “https://staging.example.com” and “https://example.com”. Our toolchain does not provide good solution for this problem, so we have to implement our own.
When I worked with Ruby On Rails I really loved its approach for database configuration:
development:
user: dev
password: dev123
# ...
production:
user: root
password: supersecurepassword
# ...
I wish I can use the same approach in my daily iOS development.
Fortunately, I can!
TL;DR;
Checkout project xcconf and Sample/Sample.xcodeproj
Commonly used mechanisms
There are at least two common approaches to separate configurations: preprocessor definitions and plist file. Both of them have serious disadvantages.
Preprocessor Definition
You may saw and probably used this approach. It’s pretty easy and simple to use:
#ifdef DEBUG
NSString *const kServerAddress = @"https://staging.example.com";
#else
NSString *const kServerAddress = @"https://example.com";
#endif
But let’s say that you add one more environment, e.g. “Beta” for beta testers. It means that you have to add new preprocessor definition and update all your constant declarations. Also, this approach is error prone: it’s really easy to make a typo or “accidentally” drop a definition. Your code will compile, but it’d use wrong environment. Though, this approach has at least one advantage: your code contains only things that you use, all the private data (addresses, keys) is not exposed.
Plist file
This is other commonly used mechanism. This approach described here. It’s also has some disadvantages: if you want to add new configuration variable - you have to add it to every subsection, also you need to add a property to class and write (copy-paste?) initialization code. Moreover, you will distribute all your keys/endpoints to the end user, or you’ll have to cleanup the file right before deploy to the AppStore. Still you have to perform way too many actions for such a simple task.
Thanks god there is more easier and robust way.
XCCONF and YAML
xcconf incorporates only best parts from methods described previously:
- all settings store in one place
- you don’t expose private data
- everything done at compile time
Here is how it looks like:
Configuration file:
principalClass: Configuration
Debug:
serverAddress: https://staging.example.com
APIKey: qwe123!!qwe
Release:
serverAddress: https://example.com
APIKey: qwe123qwe
ObjC code:
@Interface Configuration : NSObject
- (NSString *)serverAddress;
- (NSString *)APIKey;
@end
// ...
Config *config = [Config new];
NSLog(@"%@", config.serverAddress);
No preprocessor, no boilerplate!
Installation and usage
To install xcconf
you need to clone repo and run one command, it’ll build executable and install it into /usr/local/bin
git clone [email protected]:AlexDenisov/xcconf.git && cd xcconf
make install
Integration into project looks weird, but don’t worry, that’s ok :)
First of all create YAML file, e.g,: config.yaml
Put there initial configuration:
principalClass: Config
Debug:
color: Green
Release:
color: Blue
Prepare class interface for configuration. But do not create implementation. Its name should be equal to principalClass
and it might contain getter methods for all available parameters, in this case it’s just color
@interface Config : NSObject
- (NSString *)color;
@end
Let’s now compile our config, go to Build Phases
-> Compile Sources
-> +
and add config.yaml
Obviously Xcode doesn’t have YAML compiler, but we can provide our own. Open Build Rules
, add new one that matches files with pattern *.yaml
and run shell command /usr/local/bin/xcconf
.
Also, we need to specify output file, otherwise it won’t work:
$(DERIVED_FILE_DIR)/xcconf.m
. Make sure that output file has extension .m
, so that Xcode can compile it and provide implementation for the principalClass
.
Now we’re ready to use it:
#import "Config.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Config *config = [Config new];
NSLog(@"%@", config.color);
}
return 0;
}
Build&Run:
Summary
The project is very young, I did start one day ago, so it can have (it definitely has) some problems and might be improved.
I’d really appreciate any feedback.
P.S. Pull Requests are welcome! :)