Any iOS developer worth his salt has probably faced the dire realization that handling environment variables through Xcode is anything but intuitive. This post will analyze several solutions to address this issue; some taken straight out of apple’s playbook and others that people far smarter than me have come up with. In the end, I propose a new approach, inspired by everything I’ve seen and learnt thus far.

# Why should you care?

It’s a generally well regarded rule of thumb that when developing a system, one should abstract configuration specific settings from the internal code that drives the system as much as possible. The most common example of this is abstracting away the base URL used to communicate with an API or a database. In the world of web development it is common to do this with .env files, simple Key-Value lists that define environment variables that can easily be exchanged and edited without having to alter the underlying code that uses them. A popular node library that does this for you is dotenv. For the API base URL example we were using, a .env file might like something like this:

ENV_API_BASE_URL="https://my.backend.com/v1"

And we would access this value with something like Environment["ENV_API_BASE_URL"]. The cool thing about this separation is that if we wanted to be able to launch our app in a staging environment we could just create a .env.staging and populate it like so:

ENV_API_BASE_URL="https://staging.backend.com/v1"

And if all the internals were abstracted correctly, the next build would now be seamlessly communicating with our staging backend. Pretty neat concept, right? We can take it one step further.

# Keeping secrets… secret

Another neat thing about separating configurations from the code where they are used is that it frees us from tracking them in version control. If we add .env to the .gitignore (for those of you not using git for version control, there is bound to be a similar solution) that we can now store private keys and credentials as environment variables and, should our codebase ever get compromised, at least we won’t be leaking those, only our nasty coding habits.

# Are there any downsides?

There is no perfect solution for everything, and through its simplicity .env files have a glaring flaw: they do not translate into static type-checked variables in code 😞 At least not by themselves.

What do we mean by “static type-checked variables”? We mean accessing them in code with something as simple as

let apiBaseUrl: String = EnvironmentVariables.apiBaseUrl

Since we can access them with Environment["ENV_API_BASE_URL"], you’d think that it’s just a matter of wrapping them in a class or structure and assigning them, right? Well, that’s one way of handling the problem, sure, but an even cooler solution would be to have that process be automagic and populate the static type-checked variables for you.

# Can this be done in Xcode?

The short answer is Yes!, but it’s going to take a little bit of work.

First thing’s first, we need to establish the equivalent to .env files in Xcode, and that’s a Xcode Configuration (.xcconfig) file. Xcode Condfguration files have some interesting syntax quirks that make them somewhat different from .env files, but for now we’re just going to assume that they are basically the same. Thus, the following Staging.xcconfig file could serve as our Staging Configuration file:

ENV_API_BASE_URL="https:\/\/staging.backend.com/v1"

But just including this whimsical file in your project won’t cut it, the next thing we have to do is link it to a build scheme. And after that we’re about to hit the next and most crucial step in introducing environment variables into our Xcode project: Linking everything through the project’s Info.plist. This step is the crux that brings everything together when using *.xcconfig files, for you see, Environment Variables are not accessible at run-time in iOS apps, however, they are accessible at build-time and the Info.plist is the de facto to bridge both them from build-time to the run-time by assigning them to a variable. For convenience sake, we’re also going to bundle everything nicely within a dictionary, just to separate our environment variable bridges from the rest of the key-value pairs in Info.plist, and it could look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <!-- other key-value pairs -->
  <key>CurrentConfiguration</key>
  <dict>
    <key>apiBaseUrl</key>
    <string>$(ENV_API_BASE_URL)</string>
  </dict>
</dict>
</plist>

As a side note: if you know of any other way to achieve what we just covered without using the Info.plist, please let me know! All of my searches have come up empty and I suspect that it’s just my google-fu acting up.

You might be wondering if this is it, but sadly, no. Now we have to access those newly-created variables at run-time and use them in our code. A possible way to do just that could be something along these lines:

  • read the contents of the Info.plist file
  • search for the dictionary with key CurrentConfiguration
  • retrieve the value for our environment variable ENV_API_BASE_URL which has been mapped as apiBaseUrl

Which can be translated as a compact singleton class with static type-checked variables:

//
// SwiftConfigurationGenerator.swift
//
class SwiftConfiguration {

    enum ConfigurationKey: String, CaseIterable {
		  case apiBaseUrl
    }

    // MARK: Shared instance

    static let current = SwiftConfiguration()

    // MARK: Properties

    private let configurationKey = "CurrentConfiguration"
    private let configurationPlistFileName = "Info.plist"
    private let configurationDictionary: NSDictionary
    let configuration: Configuration

    // MARK: Configuration properties

    var apiBaseUrl: String {
      return value(for: .apiBaseUrl)
    }

    // MARK: Lifecycle

    init(targetConfiguration: Configuration? = nil) {
        let bundle = Bundle(for: SwiftConfiguration.self)
        guard let configurationDictionaryPath = bundle.path(forResource: configurationPlistFileName, ofType: nil),
            let configurationDictionary = NSDictionary(contentsOfFile: configurationDictionaryPath),
            let configuration = configurationDictionary[configurationKey] as? Configuration
            else {
                fatalError("Configuration Error")

        }
        self.configuration = configuration
        self.configurationDictionary = configurationDictionary
    }

    // MARK: Methods

    func value<T>(for key: ConfigurationKey) -> T {
        guard let value = configuration[key.rawValue] as? T else {
            fatalError("No value satisfying requirements")
        }
        return value
    }

And there you have it. We’ve bridged environment variables from outside of our project and into our code, all without explicitly exposing any of our secrets in code 🎉

# Going the extra mile

Of course this isn’t the end of the line. If you’re anything like me, you’re probably scratching scratching your head already, thinking about how there are so many places where you have to keep track of the environment variables (.xcconfig, Info.plist, and SwiftConfiguration.swift) and make sure that they are all well configured. Would it be great if we could just add them to the .xcconfig, map them via the Info.plist and be done with it all?

We’re gonna do just that! We’re going to write code that updates that class for us automagically! And to do that, we’ll be using the power of build phases, with which we can add a script that executes whenever we build our project, and that script can look something like this:

# Get path to SwiftConfigurationGenerator script
SWIFT_CONFIGURATION_GENERATOR_PATH=$PROJECT_DIR/SwiftConfigurationGenerator.swift

# The input configuration file
INPUT_PATH=$PROJECT_DIR/Info.plist

# The  generated file output path
OUTPUT_PATH=$PROJECT_DIR/SwiftConfiguration.generated.swift

# Add permission to generator for script execution
chmod 755 $SWIFT_CONFIGURATION_GENERATOR_PATH

# Execute the script
$SWIFT_CONFIGURATION_GENERATOR_PATH $INPUT_PATH $OUTPUT_PATH

All that’s left is to write code that will write code for us, and luckily, we can adapt the work from this repository to suit our needs. The original code wasn’t meant to read from Info.plist but rather from a Configuration.plist file somewhere in your project, and while that would also serve the same purpose, it wouldn’t make our life easier as you’ll see in the next section.

When all is said and done, what we have is a project in which we can

  • define environment specific variables in .xcconfig files
  • map them via the Info.plist into our intended in-code accessible names
  • have the corresponding accessor class be generated automatically

That’s amazing! Is there anything more you could ask for?

# How can I use this with my CI/CD?

Well, I’ve got some good news and some bad news. For starters, since we’ve been doing the best we can to keep our secrets away from our codebase, we’ll need to configure them in our CI/CD as well. The bad news is that, even though we’ve been calling them “Environemnt Variables” up to this point, we have to keep in mind that we meant that in the context of our app, not of the system building the app. “So what?” I hear you asking. Well, as we’ve alluded to in the past, we can’t access the system’s environment variables within our files until we bridge them, and .xcconfig files are not an exception to that, so we won’t be able to just write something like the following and send it off to our CI/CD.

# Environment.xcconfig
ENV_API_BASE_URL=$SYSTEM_ENV_API_BASE_URL

The good news is that we’re programmers, damn it! We don’t know the meaning of the words “It can’t be done”. Heck! That’s the whole point of this post: doing something that can’t be done out-of-the-box.

What we need is just a little bit more of metaprogramming. We need a script that will fill in those environment variables with the system’s own environment variables, and it needs to do it elegantly. Herein lies the good news, we can do it. We need

  • a template Example.xconfig file
  • a way to populate that file with the system’s environemnt variables
  • to produce a Environment.xconfig file that we can use in our app just like before

The first one is easy, we can just add a Example.xcconfig to our codebase with placeholder values for the environment variable’s values.

# Example.xcconfig
ENV_API_BASE_URL=%%ENV_API_BASE_URL%%

Next, with a little help from bash’s associative arrays, we copy the template file and inject the system’s environemnt variables into those placeholders, producing a fully working Environment.xcconfig file that we can use to build our app!

#!/bin/sh
# Source: https://clubmate.fi/replace-strings-in-files-with-the-sed-bash-command/

# Associative array where key represents a search string,
# and the value itself represents the replace string.
declare -A confs
confs=(
    [%%ENV_API_BASE_URL%%]=$SYSTEM_ENV_API_BASE_URL
)

configurer() {
    # Loop the config array
    for i in "${!confs[@]}"
    do
        search=$i
        replace=${confs[$i]}
        # Note the "" after -i, needed in OS X
        sed -i "" "s|${search}|${replace}|g" Environment.xcconfig
    done
}

# create environment .xcconfig from the example file
cp Example.xcconfig Environment.xcconfig
echo "Created Environment.xcconfig"

# execute the substitutions
configurer
echo "Injected environment variables into app's Environment.xcconfig"

Add this script as a part of your CI/CD steps and it should populate everything accordingly, just before it’s time for your app to be built using xcode. 🙌

It’s been one hell of a ride, but we’ve reached the finish line 🏁. Or have we? 🤔 Let me know if you have any more ideas on how to improve and simplify this complex process, or if I’ve made any dumb mistakes.




This was by no means intended as a complete implementation guide, if not due to the fact that most of the initial steps have been written about extensively. I’m gonna leave you with some links to materials I found useful one way or another while researching this topic.

# Auto-generated static type-checked environment variables

# .xcconfig

# Cocoapods-Keys