How to improve your Previews and Test experiences in Swift
Tags: swift, xcode, testing, previews
Published: 2023-01-01 01:07:08

## 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.
```swift
// If Not nil, it's test mode
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
```

Likewise, if a `XCODE_RUNNING_FOR_PREVIEWS` is `"1"`, it's the "Previews" mode.
```swift
// If "1", it's Previews mode
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
```

Thus, in the `AppDelegate` or `SceneDelegate`, we can like this.
```swift
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
}
```

#### 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.

```Swift
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
    }
}
```
```Swift
// AppDelegat,  SceneDelegate
if !AppConfig.isDebug {
   // Do your heavy setups. 
}
```

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

```Swift
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
```

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](https://dagger.dev/dev-guide/)" and it has nothing to do with Factory pattern or Builder pattern.

```Swift
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
    }
}

```
```Swift
// 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
```
### Pros
- All services and logic won't be exposed globally, like a`static` 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.