Takayuki Yamaguchi
Posted on January 1, 2023
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
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"
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
}
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
}
}
// 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
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" 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
}
}
// 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()
orAmplify.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.
Posted on January 1, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.