How to improve your Previews and Test experiences in Swift

cookie777

Takayuki Yamaguchi

Posted on January 1, 2023

How to improve your Previews and Test experiences in Swift

Abstract

Whenever Xcode executes SwiftUI Previews or Unit Test ( XCTest), it surprisingly runs the simulator app behind the scene. This means it also triggers the AppDelegate or ScneneDelegate even if you just want to preview or test a small part of your app. Most of the apps, especially the complicated ones, will do a lot of initial setups like Amplify, Firebase, or your own logic in those Delegate Class. This will cause a huge inefficient performance in previewing or Testing. This article tries to improve this issue by following two approaches. 

  • Flags
  • Lazy loading: Static
  • Lazy loading: Closure

 

Flags

The first approach is pretty simple. There is a way to detect if the app is running by "SwiftUI Preview" mode, "Unit test" mode, or others. We can do this by using the ProcessInfo environment. For example, if you want to know if the current run is a test mode, you can use XCTestConfigurationFilePath as follow.

// If Not nil, it's test mode
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
Enter fullscreen mode Exit fullscreen mode

Likewise, if a XCODE_RUNNING_FOR_PREVIEWS is "1", it's the "Previews" mode.

// If "1", it's Previews mode
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
Enter fullscreen mode Exit fullscreen mode

Thus, in the AppDelegate or SceneDelegate, we can like this.

let isXCTestMode = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
let isPreviewsMod= ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"

// This will not be executed on the "Test" or "Previews" mode
if !isXCTestMode and !isPreviewsMod {
   // Do some heavy setups. 
   // FirebaseApp.configure()
   // Amplify.configure()
   // Your logic's initializations
}
Enter fullscreen mode Exit fullscreen mode

Pros

  • Very simple and easy to use

Cons

  • It will be risky if Apple changes the environment specifications. Worst case, the app setups won't be triggered even in "Production" mode.
  • We need extra setup for the "Tests" and "Previews" that require those initial setups

Optimazation

We can do some optimization for this solution. To improve the first con, we can use compiler directive, and evaluate this only in debug mode.

We can also encapsulate these isXCTestMode and isPreviewsMode into some AppConfig files so that we can read from other files.

struct AppConfig {
    static var isXCTest: Bool {
        ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
    }
    static var isPreviews: Bool {
        ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
    }
    static var isDebug: Bool {
        var isDebug = false
        // 100% garauntee that this will be false in the release mode.
        #if DEBUG
        isDebug = isXCTest || isPreviews
        #endif
        return isDebug
    }
}
Enter fullscreen mode Exit fullscreen mode
// AppDelegat,  SceneDelegate
if !AppConfig.isDebug {
   // Do your heavy setups. 
}
Enter fullscreen mode Exit fullscreen mode

As of the second con, I believe it's acceptable. This is because any setups in the Test(Previews) mode should be done in those environment, and should not rely on some hidden setups. Therefore, this is why we have interface and dependency injection so that we can use and inject some mock setups and services.
 

Lazy loading: Static

Another option is to make the initialization as lazy loading, which means it will be constructed only when it's going to be used.

The easy way to do this is using a static object. Since static can be used as static, we can put our services as static.

The lazy initializer for a global variable (also for static members of structs and enums) is run the first time that global is accessed
https://developer.apple.com/swift/blog/?id=7

final class Container {
    static var serviceA = ServiceA()
}

// In the actual place which requires the service, we directly call the service. If it's the first time called, the `ServiceA` is initilizad, and after that, the object is reused.
let serviceA = Container.serviceA
Enter fullscreen mode Exit fullscreen mode

In other words, we don't do any setups in the AppDelegate or SceneDelegate.

Pros

  • Easy to implement
  • Easy to access

Cons

  • However, static can be risk as it's exposing all logic as global
  • Violating the Dependency Injection strategy

 

Lazy loading: Factory(Builder)

Instead of using static, we can use a Factory(Builder), which is just a closure to create a service. FYI, the term Factory(Builder) comes from "Dagger 2" and it has nothing to do with Factory pattern or Builder pattern.

final class LazyFactory<T> {
    private var factory: () -> T
    private var cache: T? = nil
    var dependency: T {
        get {
            // Add lock() if needed
            if let cache {
                return cache
            }
            let dependency = factory()
            self.cache = dependency
            return dependency
        }
    }

    init(_ factory: @escaping () -> T) {
        self.factory = factory
    }
}

Enter fullscreen mode Exit fullscreen mode
// In your AppDelegate or SceneDelegate
// ServiceA is not created at this moment
let factoryServiceA = LazyFactory { ServiceA() }

// In the actual place which requires the service. If it's the first time call, the `ServiceA` is initiated, and after that, the object is reused.
let serviceA = factorySerivceA.dependency
Enter fullscreen mode Exit fullscreen mode

Pros

  • All services and logic won't be exposed globally, like astatic way.
  • This means we can follow the DI pattern

Cons

  • We can not use some global setups such as FirebaseApp.configure() or Amplify.configure()

 

Conclusion

This article was how to improve Tests or Previews performance by avoiding unnecessary setups as much as possible. There is no perfect way, and it always exists a trade-off. The flag way is very simple but no flexibility. We can have a lot of control by using lazy loading, but it could be complex, and could be coupled with the DI patterns you use.

💖 💪 🙅 🚩
cookie777
Takayuki Yamaguchi

Posted on January 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related