ABOUT
TEAM
WORK
CONTACT
|
BLOG

GirAppe blog

2019/02/07 - go to articles list

Working in dynamic environment

App Environment abstraction in iOS.

In this article, I will propose an app environment abstraction. I will also show a small trick to improve your communication with testers, whenever multiple configurations come into the picture. And there is a nice bonus waiting at the end.

1. Build configurations - short recap

When you build a target in your project, you usually use some scheme (noticed this little dropdown next to the run button?). If you inspect it a bit more, you'll find, that among all the other things it does, it also ties your target (or targets) with a Build configuration. I will call it configuration

By default, new Xcode projects have two configurations:

When inspecting your target build settings, you will notice that every entry could be defined with different values for each configuration you have. You can also use it to specify (among many others):

Because of that, build configurations are often used as a decent equivalent of the app environment, whatever that means in your context. It often looks like:

Icons sample

To achieve that, all you need is to create new configurations and set the settings respectively:

Settings bundle id Settings icons

2. The problem

When talking about a development setup including multiple backend and app environments, configurations become lifesavers and a good way to set up and build an app to meet multiple requirements.

The problem arises when we start considering the main reason why we explicitly set the app environment.

They are meant to be different!

That means that Alpha version should be somewhat different to Beta, and for sure there are meaningful differences between Debug and Release. As long as it is about other API URL or some assets, it is fine. But when they differ in actual features provided, then it can backfire.

I'm sure you've seen this code at least once:

#if DEBUG
  // Do something
#else
  // Do something else
#endif

It is not that bad yet. Just have in mind, that (in most cases) the code after #else would not be checked until you start archiving or build in Release. So it already loses some compile-time safety. And considering this:

#if DEBUG
  // Debug flow
#elseif AlPHA
  // Was menat to be Alpha flow
  // But there is a typo that compiler will never highlight
#elseif BETA
  // Beta flow
#else
  // Should be Release, right?
  // unless we've added more configurations
  // and forgot to handle them
#endif

It could theoretically happen in many places in the code. Every adding/removing configuration requires to potentially revisit these places, rechecking the flow. With no help from the compiler, the code quickly becomes unmaintainable.

Another topic is unit testing - have fun writing tests for that. I had :)

3. Environment Abstraction

I was struggling with this problem in many different projects. Regardless of better or worse approaches on feature toggles, I quite often ended with something like:

public enum Environment: String {
    case debug
    case beta
    case release
    public static var current: Environment {
      #if DEBUG
      return .debug
      #elseif BETA
      return .beta
      #else
      return .release
      #endif
    }
}

...

switch Environment.current {
    case .debug:
      // flow 1
    case .beta:
      // flow 2
    case .release:
      // flow 3
}

Now there is only one place to maintain the conditional compilation. And it is so straightforward, that it makes it easier to keep it sane.

The code is always compiled as a whole, giving significantly more compile-time safety. If we add/remove environment, the compiler will highlight major problems. This model could be used to represent the app environment. And it can be extended with some common boilerplate and environment dependent variables:

extension Environment {
    var name: String { return self.rawValue }
    var appVersionNumber: String { ... }
    var appBuildNumber: String { ... }
    var apiUrl: URL { ... }  
}

And last, but not least, allow overriding Environment.current (for example for tests, but not only):

public enum Environment: String {
    case debug
    case beta
    case release
    public static var current: Environment {
        if let override = Override.current {
            return override
        }
        #if DEBUG
        return .debug
        #elseif BETA
        return .beta
        #else
        return .release
    }

    struct Override {
        static var current: Environment?
    }
}
...
Environment.Override.current = .beta

4. A small trick ;)

If you work on an app that has multiple build configurations with all that staging and pre-productions setups, or if you ever will, you'll eventually encounter this problem:

"Hi Andrzej, client send us a screenshot, could you look at it? Best,"

Nobody knows if it was taken on Alpha or Beta, not saying about the version number. Even if these data are logged in the ticket, I found them inaccurate more than once (testers are people after all, and if they take dozens of screenshots a day from multiple apps and multiple environments, it's easy to mismatch it)

As promised, there is a small trick that will make your life easier. Consider these three screenshots:

Compare small

You can spot a difference between the first two and the third one. Let me zoom that for you:

Alpha Beta

Noticed this small label? Contains everything I need to determine the app version used. The third one is empty because it is Release, which usually means it's an AppStore version. It is less dynamic due to the longer release cycle, so it should be easier to track.

It might seem kind of obvious, most of you have probably placed some version and build numbers already somewhere in the app.

This setup is a bit different though since the label is placed in separate UIWindow, even above alert level. That means that it should be visible on the screenshots from every single place in the app.

A gist with an example implementation:

Bonus - AutoEnvironment

If you were as lazy as me, and you'll be writing this Environment enum for the N-th time this year, you'll consider at least reusing it. But it's a hard job, as the configurations are dynamic and varying between projects. That's why I decided to automate the process instead.

I created a Command Line Tool in Swift, that will read your xcodeproj and generate that Environment class for you. It will also add the Swift compiler flags if they are missing. All you need to do is:

$ mint install GirAppe/AutoEnvironment
$ autoenvironment -p path/to/project.xcodeproj -o /output/dir

Add "-t target name" if the target name is different than project filename. That will generate default version of Environment.generated.swift file containing Environment enum (it's configurable - if you want to check other available options use "-h" for help).

If you want to see the version info label with the environment, just use:

// Example setup
Environment.setVersionFormat(.full)    // optional setup
Environment.info.textAlignment = .left // optional setup

if Environment.current != .release {
    Environment.info.showVersion()
}

The tool could be found at: https://github.com/GirAppe/AutoEnvironment

Thank you for reading

P.S. If you have noticed that I've just slid over feature toggles topic, it's because they are a nice feature, deserving additional article. I'm working on it now, so stay tuned.

Andrzej Michnia
Software Developer