Per-endpoint configuration

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:

Scheme (compile-time) 
  -> Build variables (compile-time) 
    -> Info.plist (compile-time) 
      -> Code (runtime)

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:

cp "${SRCROOT}/Resources/Environments/${REMOTE_ENVIRONMENT}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/${REMOTE_ENVIRONMENT}.plist"

Then you can look up that .plist in the app bundle, and get any value from it.

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.

I see two needs here then:

  1. Including/stripping information based on the flavor
  2. 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:

func endpoint() -> String {
#if MY_FLAVOUR
  return "foo"
# else
  return "bar"
#fi
}

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:

func endpoint() -> String {
#if MY_FLAVOUR
  return #ObfuscatedString("foo")
# else
  return #ObfuscatedString("bar")
#fi
}

Let me know if that helps.

1 Like