XCode Build configurations. App non-functional infrastructure Part I.

Serie introduction

The aim of this post is present a way to create a XCode Build Configuration. The following posts will also present some technics that will allow you to scale your app in several directions.

As any building for being usable needs some basic facilites (gas, water, electricity) and other that make it more confortable (e.g parking lot, laundry room,…). Same happens with mobile apps, you really need to prepare some non-functional infrastructure that will allow your app easily grow and scale.

Having different development environments (debug, staging and production), setup an start up sequence, handle different country behaviour and its translations; configure event tagging …. All of then non-functional but really important to handle them pretty well for having a good app scalability  or country expansions.

Those techniques are independent from app business, very easy to implement during first phases of development, but could be really a nightmare in case of adopting them in a mature development.

Build Configuration

The aim of this post is just to present a way to prepare your app for working with different production environments.

In simple cases, such as developing and immediately executing a program on the same machine, there may be a single environment, but in industrial use the development environment (where changes are originally made) and production environment (what end users use) are separated; often with several stages in between. This structured release management process allows phased deployment (rollout), testing, and rollback in case of problems.

Common environments for app development are:

  • Development (debug): This is environment where mobile developers build, fix, refactor the app.
  • Test (alpha): When the app is enough big it is mandatory a QA team, they will validate the app against this environment.
  • Staging (beta): This is a copy of the same environment as production one. But with the exception that data that app is handling  is not real live data. This environment is used to validate  the app before an official release.
  • Production: This is where real users are playing with the app.

… of course there could be many more depending on the scope of the app.

Hands on

It has been created a new single view project, by default XCode provides two environments Development (Debug) and Production(Release). For the purpose of this post we will only deal with three environments: Development (Debug), Staging and Production(Release). Staging has to be as close as Release so we have to duplicate release.

For being accesible build configuration (environment) value to source code. Is necessary make visible configuration in the info.plist.

AppEnvironment.swift will be the component that will handle the app configuration depending on environment:

enum Environment: String {
    case debug = "Debug"
    case staging = "Staging"
    case production = "Release"

    var toString: String {
        switch self {
        case .debug: return "dev"
        case .staging: return "staging"
        case .production: return "production"
        }
    }
}

// MARK: - Private
class AppEnvironment {

    static let shared =  AppEnvironment()

    private init() { /*This prevents others from using the default '()' initializer for this class. */ }

    lazy var environment: Environment = {

        guard let uwpInfoDictionary = Bundle.main.infoDictionary,
            let uwpConfiguration = uwpInfoDictionary["Configuration"] as? String,
            let uwpBuildConfiguration = Environment(rawValue: uwpConfiguration) else {
                print("❌❌❌ No Build configuration Found: Set Debug as default ❌❌❌")
                return Environment.debug
        }
        print("⚙️⚙️⚙️    Build configuration: \(uwpBuildConfiguration)   ⚙️⚙️⚙️")
        return uwpBuildConfiguration
    }()
}

By the moment is only returning a description string containing the environment name.

But could contain, the back end base url for each environment:

 struct ConstantsBaseURL {
        static let debug = "dev-api.aws.xxx"
        static let staging = "stage-api.aws.xxx"
        static let production = "api.aws.xxx"
    }

    var baseURL: String {
        switch self {
        case .debug: return ConstantsBaseURL.debug
        case .staging: return ConstantsBaseURL.staging
        case .production: return ConstantsBaseURL.production
        }
    }

…  the log level ….

var appLogLevel: DDLogLevel {
        switch self {
        case .debug: return .debug
        case .staging, .production: return .off
        }
    }

… api key’s values, configuration filenames (e.g. Firebase, GTM),☝️whatever that will depend on environments will be handled by this component .

Update content view just to print the environment name.

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("\(AppEnvironment.shared.environment.toString)")
    }
}

Change build configuration to staging:

Finally run the project:

Last but not least, this component fully testable.

    func test_toString() {
        XCTAssertEqual(Environment.debug.toString,"dev")
        XCTAssertEqual(Environment.staging.toString,"staging")
        XCTAssertEqual(Environment.production.toString,"production")
    }

Bundle Id

It is very important that every build configuration will also have different bundle identifiers, so in that way will be posible to deploy independently apps from different environments in the same device.

Conclusion

In this post I have presented how to create another build configuration and how take control of it from source code. You can reach the repository containing the source code here.