Runtime Environment Switcher on iOS

Ben Pust, iOS Engineer

Inside the iOS app, we have two environments: an internal environment for staging and a live app’s production environment.

Certain features only run on production; therefore, testing for those features needs to be run against the production environment. This is not an issue for the iOS team, as we can quickly point the development app to production and rebuild it, but it becomes difficult to manage with other teams testing the build. Furthermore, other engineering teams will sometimes need to test the mobile app against local backend changes, which often requires help from an iOS engineer.

To solve the aforementioned issues, we built a runtime environment switching mechanism into our app. The main requirement was to have the ability to point the app’s backend at any URL a user might choose: staging, production, local, etc.

Endpoint Controller

To start, we created an endpoint controller to manage different environments. For example, we don’t want our staging endpoint to be included in the production build, so we use preprocessor flags to exclude it.

The endpoint controller should look something like this:

#if STAGING 
static var stagingEndpoint = "staging endpoint"
#else 
static var stagingEndpoint = "" // keep it empty for production
#endif

// no matter the build, we can always keep the staging endpoint
static var stagingEndpoint = "production endpoint" 

Settings Bundle

Next, we needed to build the interface that allowed for switching. We considered making a hidden UI inside the app but decided to use the settings bundle instead. This saved us time and may save us more time down the road if we choose to add more functionality. The settings bundle is straightforward when it comes to set up. In our case, we created two settings bundle files: one that is blank and one that contains a submenu of endpoint configurations. The blank file’s settings bundle is for our production app, while the other contains the logic to modify its runtime environment.

We then added a new build phase in our app’s target to manage the two newly created settings bundles. This is a clean way to exclude the appropriate bundle based on the environment we’re building the app for! In our case, our script looks something like this:

# Type a script or drag a script file from your workspace to insert its path.
RESOURCE_PATH=$SRCROOT/DebugBundle

FILENAME_IN_BUNDLE=Settings.bundle

BUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app

echo "Checking configuration to determine whether to include Debugging Settings.bundle: CONFIGURATION=$CONFIGURATION"
echo "$CONFIGURATION || path $RESOURCE_PATH || $BUILD_APP_DIR"
if [ "$CONFIGURATION" == "Staging" ]; then
cp -r $RESOURCE_PATH/Settings-all.bundle/Root.plist $BUILD_APP_DIR/Settings.bundle/Root.plist
echo "Staging Settings-all.bundle copied to Settings.bundle"
fi
if [ "$CONFIGURATION" == "Dev" ]; then
cp -r $RESOURCE_PATH/Settings-all.bundle/Root.plist $BUILD_APP_DIR/Settings.bundle/Root.plist
echo "Dev Settings-all.bundle copied to Settings.bundle"
fi

The settings bundle should be customizable to your needs, so I won’t include it, but for this example, let’s suppose it consists of a selector that contains staging or production.

Notification Center

To detect these changes, we listen to the Notification Center’s UserDefaults.didChangeNotification. Once triggered, we check the NSNotification object to see if the notification matches the key we set in the settings bundle. On a match, we point the app to the specified endpoint. We log the user out for consistent behavior and close the app after letting the user know that a settings change has been detected. Finally, in order to determine which environment the app should enter on start, we use a simple UserDefaults storage method which we found to work consistently.

The Downsides to Using Settings Bundle and NotificationCenter

We had some trouble receiving consistent notifications from the notification center when changing the settings bundle’s endpoint. Sometimes the app would not get the notification, and nothing would happen, which could be quite confusing. From what we found, behavior depended on the state the app is in: active, background, etc. To get around this, we started fetching the settings bundle information every time we started the app and added a label to the settings bundle, letting people know to force quit the app if it does not detect it. Overall, our internal team’s approach works and saves us a significant amount of time when “rebuilding” the app within specific environments.