27

I have an iOS application that has a TestFlight build scheme. In this scheme I have setup an environment variable called TESTFLIGHT with a value of 1 set in the "Run" tab. Also, in the "Profile" tab of the build scheme it has the "Use the RUn action's arguments and variables" option checked, and I see the appropriate EV in the list.

This works just fine when running the app from Xcode, but when I make an archive and run the app on my device the Environment Variable TESTFLIGHT is not present. My question is there an option/scheme tab that I'm missing here?

The EV is set on the "Run" tab and is selected for the debug build configuration. Do I need to change this to release?

Thank you all!

miken.mkndev
  • 1,821
  • 3
  • 25
  • 39

2 Answers2

46

Environments variable are set only if you run the app from Xcode. If you run the same app from the device directly by tapping the icon, they won't be set. They are not part of the app. As their name imply, they are part of the environment, that Xcode sets up specifically before running them. (If you were developing on the Mac, you could set them in a shell before launching an executable, but that is not possible on the iPhone.)

Kyle Clegg
  • 38,547
  • 26
  • 130
  • 141
Guillaume
  • 4,331
  • 2
  • 28
  • 31
  • 3
    Thanks for your explanation. What I am trying todo is have a build scheme that I can use to build for TestFlight deployment that will have the app use a development URL to pull data from, and then have another build scheme for production that will use the production URL when the app runs. Any suggestions how this could be implemented? – miken.mkndev Jan 08 '13 at 17:21
  • 12
    Use preprocessor macros. In your target Build Settings, you need to create a new Build Configuration "TestFlight" (by default there are "Debug" and "Release"), then in Preprocessor Macros, add a new macro (for instance `TESTFLIGHT=1`) for the configuration "TestFlight". In your code, use preprocessor instructions like `#ifdef TESTFLIGHT ... #else ... #endif`. In your schemes, you can choose the "TestFlight" build configuration in the "Info" tab for action you want. – Guillaume Jan 08 '13 at 21:30
  • (To create the Build Configuration, you need to select the project, then look at the Info tab.) – Guillaume Jan 08 '13 at 21:38
  • Sweet..yeah I got that part figured out and worked correctly. Thanks for the help! – miken.mkndev Jan 08 '13 at 22:01
  • @Guillaume does it matter what the format is of the preprocessor macro? In other words, does it have to be NAME=VAL – stonedauwg Oct 12 '16 at 16:02
  • @Guillaume What if I want to point to dev server for TestFlight and point to prod server when app is actually released to app store? If my intent is to release app for test users I'd like to archive under Debug, but if its to push to app store id like to archive under Release. Will I just have to push to itunes-connect twice, once for test, and again when ready to release? – AnonProgrammer Mar 30 '18 at 00:27
3

Put env variables in build settings

If you want the environment variable to persist during archive, create a User-Defined build setting:

Edit build settings

Each target can have a different value. I've named this one RELEASE_ENV with dev, staging, and prod values for each of my targets.

Add to Info.plist

We can't read these build settings directly, but we can access them through our Info.plist.

Add an Environment variables key that exactly matches the one we created in Build Settings. Here we give it a value $(RELEASE_ENV) so that it imports the build setting we created earlier.

Edit Info.plist

You can then access it in code using:

let envDict = Bundle.main.infoDictionary?["LSEnvironment"] as! Dictionary<String, String>
let envStr = envDict["RELEASE_ENV"]!

print(envStr) // Outputs: "staging"

This is probably one of the only places you'll want to use !, that way you'll crash if the environment variable isn't there. This will persist even outside of Xcode runs.

(Optional) Set up an EnvManager to use in code

User-Defined build settings is a pretty clunky place to manager tons of separate environment variables for all your backend endpoints and other env dependent strings. You can put those in an EnvManager

WARNING: Don't store sensitive data, such as api keys, like this. Use something like AWS KMS instead.

class EnvManager: ObservableObject {
    static let shared = EnvManager()
    private init() {
        // We use ! here because we'd want to crash if missing
        let envDict = Bundle.main.infoDictionary?["LSEnvironment"] as! Dictionary<String, String>
        let envStr = envDict["RELEASE_ENV"]!
        env = EnvType(envStr: envStr)
    }
    
    @Published var env: EnvType
    enum EnvType {
        case dev
        case staging
        case prod
        
        // Read the xcode schema env var string
        init(envStr: String) {
            switch envStr {
            case "dev":
                self = .dev
            case "staging":
                self = .staging
            case "prod":
                self = .prod
            default:
                fatalError("Invalid env")
            }
        }
        
        // Make a separate computed var for each env dependent value
        var backendUrl: String {
            switch self {
            case .dev:
                return "http://localhost:8080/"
            case .staging:
                return "https://staging-api.example.com/"
            case .prod:
                return "https://api.example.com/"
            }
        }
    }
}

Usage

print(EnvManager.shared.env.backendUrl) // "https://staging-api.example.com/"
joshuakcockrell
  • 5,200
  • 2
  • 34
  • 47