coolcat stumbled upon our recommendation to not model remote environments using project configurations, and they wondered how to store things like IDs and API keys that vary per remote endpoint.
As noted in our recommendation, information can be passed from schemes down to build settings and the Info.plist of the targets being built. Environment variables flow like this:
So the information of an “environment” can be modelled as en environment variable, REMOTE_ENV=staging, and passed all the way down to your Info.plist, whose value can then be read at runtime, and used to derive other values:
enum RemoveEnv: String {
case production
case stating
static func remoteEnv() -> RemoveEnv {
if let envString = Bundle.main.object(forInfoDictionaryKey: "REMOTE_ENV") as? String {
return RemoveEnv(rawValue: envString)
} else {
return .production
}
}
}
Based on that value you can switch between API keys or endpoints.
Note that the above means you have to include your bundle metadata other than the one from the remote environment you are targeting. If you don’t want that, you can then have a .plist file per environment, and have a script build phase that copies the file based on the REMOTE_ENVIRONMENT value. Here’s some pseudo-code:
Sounds interesting, I think the detail I was missing was the ability to set a particular environment variable for a specific scheme.
I certainly don’t like the idea of bundling all my environment details for all my environments into my production app. I also don’t really like having variables in plain sight in a .plist file, where any interested party can see the details.
Packages like envied in Flutter apply some obfuscation to environment variables making it even harder for snooping eyes to find your API keys or other secrets which you have no choice but to bundle in your app.
Including/stripping information based on the flavor
Obfuscating the values of the information.
For 1, if your information can be modelled as key-value pairs, you can then use the SWIFT_ACTIVE_COMPILATION_CONDITIONS to include or not include code at compile-time:
Build settings in Tuist Projects are configured in Tuist through the Target.settings or Project.settings attributes, which take an instance of type Settings.
To address 2. you’ll have to resort to some obfuscation tool. For example the ObfuscationMacro. Combining this with the above, you’d end up with this:
My bad, I thought it’d be possible to pass compile-time environment variables from schemes, but is only possible through xcodebuild:
xcodebuild build .... FOO=BAR
You can then read the value from the build settings.
What I’d do is the following:
In development you can configure that at launch time by using scheme environment variables. They can be read at runtime, and then used in code to dynamically configure things (e.g. endpoint). Alternatively, you can build a UI into the app to switch that without having to re-launch the app, but that might require further changes in the app.
In release pass that information at compile-time as variables passed to xcodebuild, and then at compile time just copy the right data that are needed into the bundle.
Helpful, thanks. Can I suggest that this page is updated somehow because it refers to “scheme environment variables” which is quite confusing.
To be clear, I do like the general advice of avoiding creating of multiple configurations beyond plain Debug and Release (Debug-Prod etc). It’s just a question of how to achieve that.
Sometimes you need to access flavor info at build time as well as runtime. To be honest I think I prefer the StackOverflow solution above of using a script to write variables to an .xcconfig file, rather than specifying variables in multiple places as you suggest.
After all these years, flavoring is still so much easier and more elegant in Androidland!