Continuous Integration Environment Variables in iOS projects using Swift

In today’s article, we are going to talk about a very common issue that we as iOS developers sometimes have to deal with in our development workflow. That problem is how to store our apps production API keys for services that we use, our development, testing or production server URL’s and other data that are maybe too sensitive to be added inside our repository with the code.

Also how to separate the Continuous Integration pipelines to do production, testing or development builds that uses different API keys, access keys, services API address or any of those sensitive data.

There are any approaches on how to solve that issue. Including those described on the this amazing article named Secret variables in Xcode AND your CI for fun and profit by the awesome Flawless App team that in fact was one of the inspirations for this one and this little project called Swift Variable Injector.

Also, this thread where people were asking the same questions than me when I was doing a research was another inspiration for this.

https://forums.xamarin.com

The example approach that we are going to show below is very similar to the one described in [1] that uses Sourcery and code generation to create swift files based on the environment variables values and insert those into the project on a Continuous Integration pipeline.

We are now going to demonstrate an example of how we are going to use Swift Variable Injector to perform literal value substitution injecting environment variable values into swift code in a Continuous Integration Pipeline.

So …. let’s start

First with our project created let’s define the classes that we will use to handle the environment variables and Continuous Integrations values.

First, the Environment struct, in this case, is to encapsulate the environment we are using because for development we can take advantage of Xcode’s environment variables and put your development keys there. Example:

NOTE: Those Xcode Environment Variable parameters are passed to the Process (App) at launch time and not at compile time. Meaning that you launch the process with those arguments, that you are only going to have those values if you run the project with Xcode. It’s typically only useful for development purposes. If you want to be able to launch a deployed version without using Xcode, you can replace the CI class from the var pattern to your key value. But it is important not to let it static defined when you commit and push it. :))

The CI.swift class declares the variables to be substituted on the variable injector step of the Continuous Integration.

Note: Is important that the literal values matches the formatt $(ENV_VAR_NAME).

On the Continuous Integration side we have to define an environment variable with the values for it in each workflow (Production, Testing, …).

I’m going to use Bitrise service to demonstrate the steps here. But all the functionalities needed are provided by most of the CI services.

Here we are basically defining environment variables for each workflow. So if we have, for example, a Production workflow and a Beta Testing one, you’ll define the key on both but with different values.

After defining the variables or secrets for each workflow. We now can add our variable injection step before our build step.

You can do this both ways:

Using shell script

You can define a step before build to run the following script to install and run the variable injector:

# Installing 
cd /tmp
curl -OL https://github.com/LucianoPAlmeida/variable-injector/releases/download/0.2.0/x86_64-apple-macosx10.10.zip
unzip x86_64-apple-macosx10.10.zip
cp -f x86_64-apple-macosx10.10/release/variable-injector /usr/local/bin/variable-injector
cp -f x86_64-apple-macosx10.10/release/libSwiftSyntax.dylib /usr/local/lib/libSwiftSyntax.dylib
#Running 
variable-injector --file ${SRCROOT}/Folder/CI.swift --verbose

Or you can download and run the install-binary.sh file on the project repo.

You only need to pass one or more files that you have the variables to be replaced, in this example is CI.swift. And optionally a verbose mode flag.

Note: The verbose mode you print the values of your environment variables on the logs. So you may be careful and use it only for debug porpuses.

So after running the step your file now you look like this

Note: For more information about what is going to be done on the replacement check out the Swift Variable Injector repo.

Using Bitrise Step

If you are using Bitrise as Continuous Integration Service you can add the Swift Variable Injector Step on your workflow.

https://www.bitrise.io

You can find the Swift Variable Injector Step available on bitrise when adding a step to your workflow.

Adding the Injection as a Run Script Build Phase

Another way we can inject the variables is just installing the Swift Variable Injector and add the script call for variable replacement on your build phases.

Important: Is very important to add this Run Script phase before the Compile Sources phase. So the variables will be replaced and then compiled :))

Xcode build script

This way we can set up our Development keys in our local machine as environment variables and use those instead of Xcode Environment variables as explained before. And your CI step the only thing you have to do is set the variables and install the Swift Variable Injector before build. It seems an easier way and let the replacement as part of your project instead of a CI step. I believe it is a good way because we don’t rely on launch arguments that just work when we launch using Xcode.

You can find this script to copy on the project repo Readme Section Using as Run Script in Build Phases.

Note: Keep in mind that the variable injector process is lauched by the Xcode on the build phases and not the terminal, so variables on your ~/.bash_profile are not visible to GUI or CLI processes. So make sure to set those variables on lauchctl and then restart Xcode process.

There’s more info about environment variables being loading by processes on macOS on this thread.

So the values were injected on our swift files …

After that, the sources are edited by the variable injector with the environment variable values and we just have proceed with our normal Continuous Integration and Continuous Delivery process.

Conclusion

That is just another approach to solve a common problem that we iOS developers have. It just makes it is as simple as the other approaches that we mention before and as what the Android folks have with Gradle to get the system environment variables at build time. 
That was a kind of experimental project, and maybe there are some edge cases that we may encounter along the way that it handle well. But, I believe the main problem is solved using those steps.

That’s all for this article, hope you like it :)

If I got something wrong or you got some comment or question, please let me know. I will be really happy in receiving your feedback :)

You can find me on twitter at @LucianoPassos11

Thanks for reading :)

References

  1. Secret variables in Xcode AND your CI for fun and profit. https://medium.com/flawless-app-stories/secret-variables-in-xcode-and-your-ci-for-fun-and-profit-d387a50475d7
  2. Apple Open Source Project SwiftSyntax. https://github.com/apple/swift-syntax
  3. Bitrise.io Documentation. https://github.com/bitrise-io