Let’s immediately localize our projects

New project? Whether you need it or not, start localizing immediately. It does not matter that your app is only in English at launch, sooner or later, you’ll need to internationalize your app. So you’ll only see the benefits later, when you are app picks up and you suddenly find the need to translate it.

Good news, with a few simple tricks that can be started when the project is created or soon after:

  • you won’t have to open all your screens one by one to find what needs to be localized, it’s already done
  • you will find all localized strings in your catalog, ready to be sent to somebody for translation
  • bonus: if you work for a company, impress your boss by giving a ridiculously low estimate for how much time it will take to localize, the work was done incrementally, and it’s not going to be a huge project to support a new language

Add a String Catalog to your project

In the file explorer of Xcode, right-click in your Resources or Supporting Files folder, click “New File…”, and add a String Catalog called “Localizable”.

That’s it, you are ready to localize.

Adopt a naming convention for your keys

Keep it simple, my keys are always structured the same, with a bit of consistency, you will make it easy to find what you want, and maintain your project.

The convention I recommend is:

  • the first segment is the name of the screen
  • each segment gets more specific about where the string is going to be used and what it represents
  • each segment of your key is written using snake_case
  • segments are separated by a dot .

For example:

  • “editor.title” represents the title of the Editor screen
  • “editor.add_frame_button.title” represents the title within

Write a shorthand to transform a key into a localized string

Writing NSLocalizedString every time you need it and adding a comment isn’t. It’s dead simple, start by creating a new String+i18n.swift file.

Add a basic extension to transform a string into a localized string:

import Foundation

extension String {
    var i18n: String { NSLocalizedString(self, comment: "") }
}

And then to use it in code:

func viewDidLoad() {
    super.viewDidLoad()

    // i18n
    title = "editor.title".i18n // Editor
    addFrameButton.setTitle("editor.add_frame_button.title".i18n, forState: .normal) // Add Frame
}

And that’s pretty much it. Never hardcode an interface string and always use your shorthand function to localizable each and every string, it’s an overhead of approximately 30 seconds per screen that will benefit you in the long run. This will work with UIKit & SwiftUI.

Deploying a Swift Vapor app to Railway

I am a big fan of Railway, it allows me to deploy any sort of backend and database with almost no effort and the pricing is extremely reasonable to start, especially given that my prototypes and work-in-progress projects have no traffic. The free $5 per month they give makes it basically free to experiment with, I’ve been using the platform for a few months and didn’t have to pay anything yet!

I’m usually developing my backends in JavaScript/TypeScript, but as an iOS and Swift developer, I wanted to toy around with Swift Vapor and answer the question: can I use Railway to deploy a Swift app?

The answer is yes and it’s extremely simple, let’s try it.

Setup a Vapor project

I followed the official Vapor documentation:

  1. Xcode was already installed
  2. installed the vapor toolbox using: brew install vapor
  3. created a new project using: vapor new hello -n
  4. build the project in Xcode and verify that it works in the browser at http://127.0.0.1:8080

So far so good!

Create a GitHub project

Here I simply created a new private repository on Github, nothing special. I then added the remote to the Vapor project and pushed my code to the main branch.

Setup Railway

After creating a free account on Railway, let’s create a new project:

You will need to connect your Github account for Railway to detect your available projects. Once done you can pick the GitHub repository we created earlier:

Then click “Deploy Now”.

It’s kind of magic at this point, Railway automatically detects the Dockerfile at the root of the project, builds and deploys the project… and after a few minutes, it almost works at this stage.

Now an important step, you need to set up an environment variable for the port exposed by Docker, go to Variables, and add a new PORT variable using 8080 for the value:

On save, Railway automatically re-deploys, let’s wait another few minutes, almost there!

Now that we have added the port, Railway automatically detects a web server and offers to associate a domain, click “Add Custom Domain”:

Here you can either connect your own domain (for free!) or use a railway.app subdomain, for the demo let’s click “Enable” for the railway subdomain:

And just like that, we have our Swift Vapor app running in the cloud:

Building a simple feature flag system in Swift with Firebase Remote Config

In this post, we will see how we can create a simple feature flag system in Swift leveraging the Firebase Remote Config.

Feature flags (also sometimes called feature gates) are a simple mechanism that allows for enabling/disabling a feature at runtime. Here are some interesting use cases of feature flags:

  • only enable a feature for certain users meeting certain criteria: for example, I want to enable this feature, but only for users who live in the United States or have their phone language set to English
  • develop a feature and merge your code to the main branch freely as you know it will not be available to the user until you are ready to enable the feature
  • disable a feature remotely, without having to re-deploy your clients (and in the case of iOS re-submit a new version and wait for users to update their apps)

Before we begin

The following assumes that you already have Firebase installed and configured in your project, if not, please follow the instructions at https://firebase.google.com/docs/ios/setup).

In the Firebase console, let’s create our first dynamic configuration to represent a feature gate. I’m going to call this one enable_learn_mode_v2. For this, let’s go to the Firebase console, then Remote Config, and click on “Add parameter”. Set the key of the feature flag in the “Parameter name (key)” field, the Data type is going to be Boolean, and the default value false.

Remote configuration

A remote configuration is the first prerequisite to creating a dynamic feature flag system. We are going to create an abstraction that allows us to retrieve a dynamic configuration, and use Firebase Remote Config as its first implementation, this way we can easily migrate to another technology when we need to.

First, let’s create an enum that will encapsulate our different configuration keys:

// Configuration.swift

import Foundation

// MARK: - Configuration
enum Configuration {
    case custom(key: String)
    /* insert future configuration keys here */
    
    var key: String {
        switch self {
        case let .custom(customKey):
            return customKey
        }
    }
}

We can now create a ConfigurationProvider protocol to define how we will want to use the remote configuration:

// ConfigurationProvider.swift

import Foundation

// MARK: - ConfigurationProvider
protocol ConfigurationProvider {
    func boolean(for configuration: Configuration) -> Bool
    func double(for configuration: Configuration) -> Double
    func integer(for configuration: Configuration) -> Int
    func string(for configuration: Configuration) -> String?
    func string(for configuration: Configuration, defaultValue: String) -> String
}

Most components in our app will only need to use this ConfigurationProvider, but the app itself will need to make sure it refreshes the configuration at launch, let’s create a ConfigurationManager protocol for that:

// ConfigurationManager.swift

import Foundation

// MARK: - ConfigurationManager
protocol ConfigurationManager: ConfigurationProvider {
    func refresh() async throws
}

And now that we have all the building blocks, we can create our implementation using Firebase:

// FirebaseConfigurationManager.swift

import Firebase

// MARK: - FirebaseConfigurationManager
final class FirebaseConfigurationManager: ConfigurationManager {
    enum Exception: Error {
        case unknownFetchError
    }
    
    // MARK: - Remote config management
    func refresh() async throws {
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            remoteConfig.fetch { (status, error) in
                switch status {
                case .noFetchYet, .throttled, .success:
                    continuation.resume(returning: ())
                case .failure:
                    continuation.resume(throwing: error ?? Exception.unknownFetchError)
                @unknown default:
                    assertionFailure("Unsupported case when refreshing the Firebase remote configuration")
                }
            }
        }
    }
    
    // MARK: - Value processing
    func boolean(for configuration: Configuration) -> Bool {
        remoteConfig[configuration.key].boolValue
    }
    
    func double(for configuration: Configuration) -> Double {
        remoteConfig[configuration.key].numberValue.doubleValue
    }
    
    func integer(for configuration: Configuration) -> Int {
        remoteConfig[configuration.key].numberValue.intValue
    }
    
    func string(for configuration: Configuration) -> String? {
        remoteConfig[configuration.key].stringValue
    }
    
    func string(for configuration: Configuration, defaultValue: String) -> String {
        string(for: configuration) ?? defaultValue
    }
    
    // MARK: - Dependencies
    private let remoteConfig: RemoteConfig = RemoteConfig.remoteConfig()
}

It’s now time to initialize our configuration manager. The AppDelegate is a good place to initialize and refresh the configuration, and you may only want to really access the app once this step is done:

// AppDelegate.swift

import Firebase
import UIKit

// MARK: - AppDelegate
final class AppDelegate: UIResponder, UIApplicationDelegate {
    private lazy var configurationManager: ConfigurationManager = FirebaseConfigurationManager()
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Initialize Firebase
        FirebaseApp.configure()
        
        // Refresh the remote configuration
        Task {
            do {
                try await configurationManager.refresh()
                // only now the app really ready to start
            } catch {
                print("Error: failed to refresh the remote configuration.")
            }
        }

        return true
    }
    
    ...
}

Providing feature gates

In a similar way, we are going to create an abstraction for the feature flag system that uses our ConfigurationProvider as a data source.

We start with an enum that will encapsulate our feature flag keys. The string associated values will be the configuration keys:

// FeatureGate.swift

import Foundation

// MARK: - FeatureGate
enum FeatureGate: String {
    case enableLearnModeV2 = "enable_learn_mode_v2"
    
    var key: String { rawValue }
}

And as you will see, the feature gate provider implementation is quite simple:

// FeatureGateProvider.swift

import Foundation

// MARK: - FeatureGateProvider
protocol FeatureGateProvider {
    func isGateOpen(_ featureGate: FeatureGate) -> Bool
}

// MARK: - DefaultFeatureGateProvider
final class DefaultFeatureGateProvider: FeatureGateProvider {
    // MARK: - Initializers
    init(configurationProvider: ConfigurationProvider) {
        self.configurationProvider = configurationProvider
    }
    
    // MARK: - Gate management
    func isGateOpen(_ featureGate: FeatureGate) -> Bool {
        configurationProvider.boolean(for: .custom(key: featureGate.key))
    }
    
    // MARK: - Dependencies
    private let configurationProvider: ConfigurationProvider
}

Usage

We now have a FeatureGateProvider class we can inject in our code (view controllers, view models, presenters, etc.) to determine if a feature is enabled or not. Then it’s a simple matter of writing a bit of conditional code:

if featureGateProvider.isGateOpen(.enableLearnModeV2) {
  // new behavior to only be active when the gate is open
} else {
  // default behavior for when the gate is closed
}

Unit testing

Since we have created protocols for most things, it’s going to be very easy to mock our feature flag system in unit tests:

// ConfigurationManagerMock.swift

@testable import FeatureGateSystem

// MARK: - ConfigurationManagerMock
final class ConfigurationManagerMock: ConfigurationManager {
    private(set) var refreshCallCount: Int = 0
    func refresh() async throws {
        refreshCallCount += 1
    }
    
    var booleanOverride: Bool = false
    private(set) var booleanCallCount: Int = 0
    func boolean(for configuration: Configuration) -> Bool {
        booleanCallCount += 1
        return booleanOverride
    }
    
    var doubleOverride: Double = 0.0
    private(set) var doubleCallCount: Int = 0
    func double(for configuration: Configuration) -> Double {
        doubleCallCount += 1
        return doubleOverride
    }
    
    var integerOverride: Int = 0
    private(set) var integerCallCount: Int = 0
    func integer(for configuration: Configuration) -> Int {
        integerCallCount += 1
        return integerOverride
    }
    
    var stringOverride: String? = nil
    private(set) var stringCallCount: Int = 0
    func string(for configuration: Configuration) -> String? {
        stringCallCount += 1
        return stringOverride
    }
    
    private(set) var stringWithDefaultValueCallCount: Int = 0
    func string(for configuration: Configuration, defaultValue: String) -> String {
        stringWithDefaultValueCallCount += 1
        return defaultValue
    }
}
// ConfigurationProviderMock.swift

@testable import FeatureGateSystem

// MARK: - ConfigurationProviderMock
final class ConfigurationProviderMock: ConfigurationProvider {
    var booleanOverride: Bool = false
    private(set) var booleanCallCount: Int = 0
    func boolean(for configuration: Configuration) -> Bool {
        booleanCallCount += 1
        return booleanOverride
    }
    
    var doubleOverride: Double = 0.0
    private(set) var doubleCallCount: Int = 0
    func double(for configuration: Configuration) -> Double {
        doubleCallCount += 1
        return doubleOverride
    }
    
    var integerOverride: Int = 0
    private(set) var integerCallCount: Int = 0
    func integer(for configuration: Configuration) -> Int {
        integerCallCount += 1
        return integerOverride
    }
    
    var stringOverride: String? = nil
    private(set) var stringCallCount: Int = 0
    func string(for configuration: Configuration) -> String? {
        stringCallCount += 1
        return stringOverride
    }
    
    private(set) var stringWithDefaultValueCallCount: Int = 0
    func string(for configuration: Configuration, defaultValue: String) -> String {
        stringWithDefaultValueCallCount += 1
        return defaultValue
    }
}
// FeatureGateProviderMock.swift

@testable import FeatureGateSystem

// MARK: - FeatureGateProviderMock
final class FeatureGateProviderMock: FeatureGateProvider {
    var isGateOpenOverride: [FeatureGate: Bool] = [:]
    private(set) var isGateOpenCallCount: Int = 0
    func isGateOpen(_ featureGate: FeatureGate) -> Bool {
        isGateOpenCallCount += 1
        return isGateOpenOverride[featureGate] ?? false
    }
}

Enabling isolated testability for your iOS project

By default with iOS and Swift, the test target runs your actual application in a simulator, but this can be a problem because you usually don’t want to run the entire launch sequence when running unit tests. The launch sequence may trigger analytics events, or pull data from an API for no good reason since we are only interested in running our tests in the most isolated environment possible.

To avoid this situation, we are going to tell Xcode to launch our application from a dummy entry point, only for test purposes. This testing launch sequence will do… nothing, and that’s the point!

Step 1: Removing existing @main modifiers

By default, Xcode uses a the @main annotation to know how to launch the application. This annotation is automatically added by the Xcode project template in the AppDelegate. In order to change the entry point of the application during unit tests, let’s remove the annotation altogether. To do that, change your `AppDelegate.swift` from:

// AppDelegate.swift

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
}

to:

// AppDelegate.swift

final class AppDelegate: UIResponder, UIApplicationDelegate {
    ...
}

Step 2: main.swift

Xcode now needs a way to know what the entry point of the application is, if not using the @main annotation, you use UIApplicationMain function from a special file named main.swift. This file is optional and doesn’t exist by default in your app, so let’s create it:

// main.swift

import UIKit

let appDelegateClass: AnyClass = NSClassFromString("TestingAppDelegate") ?? AppDelegate.self
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(appDelegateClass))

With this code, we say: try to use the TestingAppDelegate as a starting point for the application, but if it does not exist (and this file will only exist in the test target), use the standard AppDelegate.

Step 3: Creating test utilities

Let’s fill in the blanks by creating the TestingAppDelegate and associated classes. This special AppDelegate will not do anything besides connect to a TestingSceneDelegate that itself will do nothing besides adding a dummy view controller as its root.

In your test target, create a new Test Utilities folder and add the following 3 new files to it:

  • TestingAppDelegate.swift
  • TestingSceneDelegate.swift
  • TestingViewController.swift
// TestingAppDelegate.swift

// MARK: - TestingAppDelegate
@objc(TestingAppDelegate)
final class TestingAppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let sceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
        sceneConfiguration.delegateClass = TestingSceneDelegate.self
        sceneConfiguration.storyboard = nil
        return sceneConfiguration
    }
}
// TestingSceneDelegate.swift

import UIKit

// MARK: - TestingSceneDelegate
final class TestingSceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else {
            return
        }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = TestingViewController()
        window?.makeKeyAndVisible()
    }
}
// TestingViewController.swift

import UIKit

// MARK: - TestingViewController
final class TestingViewController: UIViewController {
    override func loadView() {
        let label = UILabel()
        label.text = "Running unit tests..."
        label.textAlignment = .center
        label.textColor = .white
        label.backgroundColor = .black

        view = label
    }
}

Step 4: work around the Xcode caching issue

Since the introduction of SceneDelegate in iOS 13, testing was made a tiny bit more complicated due to the iOS operating system caching the SceneDelegate across sessions. This is a problem because switching from the actual app running in the simulator to the unit tests running in the same simulator results in Xcode being confused and using the cached version instead the one we decided to use in AppDelegate or TestingAppDelegate.

To bypass this issue, you can manually kill the app and remove it from the app switcher in between sessions, but the easiest way to always try to run the tests in a different simulator (eg: I used to run the app in a iPhone 14 Pro simulator, but the tests in an iPhone 14). Unfortunately, this is still error-prone and you always end up running the wrong scheme in the wrong simulator.

But I recently came across this solution that helped solve this problem by removing all cached scene configurations in the testing AppDelegate to ensure the configuration declared TestingSceneDelegate is used. We need to modify our TestingAppDelegate to add the following code:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Remove any cached scene configurations to ensure that TestingAppDelegate.application(_:configurationForConnecting:options:)
    // is called and TestingSceneDelegate will be used when running unit tests.
    // NOTE: THIS IS PRIVATE API AND MAY BREAK IN THE FUTURE!
    for sceneSession in application.openSessions {
        application.perform(Selector(("_removeSessionFromSessionSet:")), with: sceneSession)
    }

    return true
}

As noted, this is using a private API! Normally, you would want to avoid using private APIs because Apple will systematically detect them during App Review and reject your submission to the App Store. But in this case, the private API is used in the unit tests target, and since this code never reaches the App Store, it is 100% safe to use.

It’s done

Now when running the unit tests, the app that launches in the simulator isn’t your full app, it’s this nicely isolated dummy app that simply says “Running unit tests…”.

Happy testing!

An elegant way to update variables without unwrapping optionals using custom Swift operators

In Swift, you may want to update the value of a variable only if it is not nil. This can be cumbersome to do with regular assignment syntax, as you have to first unwrap the optional value and check if it is not nil before performing the assignment.

let originalStory: Story? = ...

if let originalStory = originalStory {
    story.basedOn = originalStory
}

By creating a custom operator, you can easily and elegantly perform this type of conditional assignment in a single step. Creating a custom operator ?= can help make your code more concise and maintainable, here is the code for the operator:

infix operator ?= : AssignmentPrecedence

func ?= <T>(lhs: inout T, rhs: T?) {
    if let value = rhs {
        lhs = value
    }
}

This operator is using generic and can be used with variables of any type, so long that the left and right operands are of the same type.

You could use this operator to update the value of a variable only if the right operand is not nil, like this:

var x = 1
x ?= nil // x remains 1
x ?= 2 // x is now 2

This operator uses the AssignmentPrecedence precedence group, which means that it will have lower precedence than the assignment operator (=).

One of the most useful uses I find in this custom operator is when updating the content of a dictionary if and only the variables are not nil. For instance in this code where both game.clef and game.mode are optional and could be nil:

var properties: [String: Any] = [:]
if let clef = game.clef {
    properties["clef"] = clef
}
if let mode = game.mode {
    properties["mode"] = mode
}

becomes:

var properties: [String: Any] = [:]
properties["clef"] ?= game.clef
properties["mode"] ?= game.mode