A common solution to supporting multiple environments is to use Xcode Conﬁgurations and Schemes. This is the recommended approach from the top search results on the subject.
An Xcode Scheme is created for each environment (e.g. Development, Test, UAT, Staging, Production). Each Scheme then uses a different Build Conﬁguration. Every single Build Setting can be conﬁgured differently for each Conﬁguration - including Architectures; Base SDK; whether On Demand Resources are enabled; whether Bitcode is enabled; which Info.plist to use; which Provisioning Proﬁle to use; or what the Bundle Identiﬁer will be.
Any of these settings, and a whole host of others, can be changed per Conﬁguration. In doing so, you run the risk of – at best – not being able to install the app on your device after archiving it and distributing an inhouse build. At worst, you run the risk of releasing to the App Store with unwanted settings.
Also, if you have a legitimate reason to change a particular setting for all of your "release" environments, then you need to change it in multiple places.
One example of an unwanted setting reaching the App Store is the Supported Interface Orientations, deﬁned in the Info.plist. The app could go through all levels of testing, with videos veriﬁed as working correctly in landscape. But then you come to install the version you released to the App Store and it does not support playing video in landscape. This is because a different Info.plist was used during testing.
So really, we want to ensure that the Xcode Build Settings we are testing against are the exact same settings that we release the app with.A clear advantage to this approach is that it is simple to change environment during development by changing the Build Scheme in Xcode.
Common safeguards still have their disadvantages
Developers would usually limit what they change per Conﬁguration to a list of User-Deﬁned Build Settings (although there is no way to enforce this restriction).
User-Deﬁned Build Settings can be referenced from the Info.plist. This way you don't have to create a separate Info.plist for each environment, you would have a single default Info.plist from which you reference your custom settings. Plus you can deﬁne custom settings for each environment supported.
However, because Xcode does not defend against changing other Build Settings, you still risk encountering the issues described above.
Another disadvantage to User-Deﬁned Build Settings is that they are all deﬁned as strings, with no validation of what has been input. Also, because all of your settings are entered as strings, you need to convert them to the data type you're actually interested in, which might be a Boolean or a URL, and we need to write repetitive code to convert each setting.
So you might have a setting that contains the URL for the main entry point into your backend. If this is an invalid URL that cannot be parsed to create an NSURL instance, then you won't ﬁnd out until you run the app.
Wouldn't it be nice to know about this error when compiling?
There are various other solutions to the multi-environment problem. One of which is to use GCC pre-processor macros. However, this is a messy approach which usually requires deﬁning macros in a Conﬁguration Settings File (a.k.a. .xcconﬁg) for development, and passing as command line arguments from a build script for release.
It's a little messy to set up, but can be done without the need for multiple Conﬁgurations. This means that you can do all of your development on the default "Debug" Conﬁguration, and carry out all levels of testing against the "Release" Conﬁguration, by injecting the pre-processor macro based settings at compile time.
A major disadvantage to this approach is that in development, we change environments by commenting out in our Conﬁguration Settings File. However, this is not an ideal way to change conﬁguration.
The solutions described above all require you to develop some kind of AppEnvironment class (or struct) to read the settings from the Info.plist or from another Plist or ﬂat ﬁle, or to create settings from the preprocessor macros. And all implementations of such class that I've seen have contained other logic to convert from strings into various data types, including URLs. Ultimately, we want a solution that does not require this component to be implemented manually.
Every time you add a new setting, you need to add it to your property list or ﬂat ﬁle, and manually edit the code of your (e.g.) AppEnvironment ﬁle to add a property to wrap/hide the access to the property list, such as