Considering to iOS development, The Internet contains a lot of materials on “How to do applications”, but much less about effective work organizing. Development is not only about writing ideal code. It’s a good idea to minimize the time spent on programming related tasks. Some tricks, automation and using hidden (actually, most of them are not hidden) powers of your IDE can save you up to one hour a day.

The problem

Server API based applications usually need to use different API endpoints for different purposes. For instance, you have Production, Development and Staging endpoints. Each of which has different configs set. Let it be APIEndpointURL and loggingLevel. These configs should be different for each environment.

Usually programmers deal with it by commenting and uncommenting values in config file. Config file then looks kinda like this:

// MARK: - Development
let APIEndpointURL = "http://mysite.com/dev/api"
let loggingLevel = "VERBOSE"

// MARK: - Production
// let APIEndpointURL = "http://mysite.com/prod/api"
// let loggingLevel = "NONE"

// MARK: - Staging
// let APIEndpointURL = "http://mysite.com/staging/api"
// let loggingLevel = "ERROR_ONLY"

As you see, we are now on Development environment :) It’s dirty, messy and error-prune. We need a convenient and safe way to work with different environments.

Xcode schemes

Using the Xcode schemes seems to me as the logical and convenient method to deal with multiple environments. First of all, create the new project using Single View Application option. I named my project MultiEnvSample.

At first, let’s create needed build configurations. Click project icon, select the project in the PROJECT section, click + icon. (fig. 1).

fig. 1

fig. 1

In the popup select which of the available configurations you want to duplicate. Duplication saves you from setting up configuration from scratch. I have created three configurations: Production – duplicates Release, Staging and Development – both duplicates Debug. (fig. 2)

fig. 2

fig. 2

Go to the upper left corner, press the button with your apps name (to the right of the Stop button), create new build scheme for each configuration. For the purpose of convenience, I gave them the same names. (fig. 3)

fig. 3

fig. 3

To hide the unwanted scheme from the list, select Manage Schemes from the schemes menu. Uncheck unwanted scheme. (fig. 4)

fig. 4

fig. 4

For each of your schemes repeat the following steps:
Select scheme and hit Edit Scheme menu option. (fig. 5)

fig. 5

fig. 5

In the next window select Run section, and select build configuration with the same name, as the scheme has. (fig. 6). Remember, that using this menu, you can always set up different options for each of your build configuration.

fig. 6

fig. 6

Now schemes configuration is over. Let’s add some code. Go to your Info.plist file. Add new String key, call it Config. Set $(CONFIGURATION) as the value. (fig. 7)

fig. 7

fig. 7

The last step needs some additional explanations. To build the app, compiler accepts different settings. These settings are listed in the Documentation. Or, if you want to see just a list, you can do it here on StackOverflow. The $(CONFIGURATION) variable stores the name of the current configuration.

To test it select, for instance, Development scheme from the list in the top left corner of the XCode. Then go to AppDelegate.swift and modify application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) method as it shown on the sample below. Run the project.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  // `Config` is the name of your `Info.plist` key
  print(NSBundle.mainBundle().objectForInfoDictionaryKey("Config"))
  return true
}
// > Prints Optional(Development)

Test if it works for all the others configurations. In case of errors, check if you followed all the steps above.

Create new Swift class. Call it Config. Add the following code to Config.swift. This class is one of few rare cases, when Singleton is needed.

import UIKit

class Config: NSObject {
  // Singleton instance
  static let sharedInstance = Config()
  
  var configs: NSDictionary!
  
  override init() {
    // Take current configuration value from the `Info.plist`. `Config` is the name of the Info plist key (fig. 7). 
    //`currentConfiguration` Can be `Debug`, `Staging`, or whatever you created in previous steps.
    let currentConfiguration = NSBundle.mainBundle().objectForInfoDictionaryKey("Config")!

    // Loads `Config.plist` and stores it to dictionary. The configuration names are the keys of the `configs` dictionary.
    let path = NSBundle.mainBundle().pathForResource("Config", ofType: "plist")!
    configs = NSDictionary(contentsOfFile: path)!.objectForKey(currentConfiguration) as! NSDictionary
  }
}

Let’s add some helper methods to get config values.

extension Config {
  func apiEndpoint() -> String {
    return configs.objectForKey("APIEndpointURL") as! String
  }
    
  func loggingLevel() -> String {
    return configs.objectForKey("loggingLevel") as! String
  }
}

Now config properties are available from any place of your code.

// Prints Api Endpoint depending on current scheme.
print(Config.sharedInstance.apiEndpoint()) 

Important!

Remember, that data in the *.plist file can be readed and is potentially unsafe! (thanks @jcampbell_05 for friendly reminder). Yet, you can create config for the secrets in the code, and then store just the key in the *.plist using the same access principle.

Conclusion

It’s always a good idea to prevent errors at the very start of the development. Using some simple techniques we can improve development quality, make routine tasks less painful and save the time.

Where to go from here?

Download Sample Project from GitHub. Try to play with different scheme settings. Think about tests integration. How would you do it? Also read Apple Official documentation.