The text is clear and conveys the intended message effectively. However, it can be slightly refined for improved readability and flow. Here's a polished version: In our previous post, we discussed migrating an app that uses a Singleton to Swift 6.0. In this post, we’ll focus on consolidating multiple multipurpose Singletons into a single access point. This approach simplifies unit testing by enabling the injection of mocked Singletons.
Base project
We begin where we left off in the iOS Location Manager: A Thread-Safe Approach post. In that post, we explained how to migrate a Location Manager. Now, we’ll create a new blank project, ensuring that the Swift testing target is included.

The base code is the source code provided in the commented section of the post. At the end of the post, you will find a link to the GitHub repository. By reviewing its history, you can trace back to this point.
At this stage, we will create a second singleton whose purpose is to manage a long-running background task.
@globalActor
actor GlobalManager {
static var shared = GlobalManager()
}
protocol LongTaskManagerProtocol {
@MainActor var isTaskDone: Bool { get }
func doLongTask() async
}
@GlobalManager
class LongTaskManager: ObservableObject, LongTaskManagerProtocol {
@MainActor
static let shared = LongTaskManager()
@MainActor
@Published var isTaskDone: Bool = false
private var isTaskDoneInternal: Bool = false {
didSet {
Task {
await MainActor.run { [isTaskDoneInternal] in
isTaskDone = isTaskDoneInternal
}
}
}
}
#if DEBUG
@MainActor
/*private*/ init() {
}
#else
@MainActor
private init() {
}
#endif
// MARK :- LongTaskManagerProtocol
func doLongTask() async {
isTaskDoneInternal = false
print("Function started...")
// Task.sleep takes nanoseconds, so 10 seconds = 10_000_000_000 nanoseconds
try? await Task.sleep(nanoseconds: 10_000_000_000)
print("Function finished!")
isTaskDoneInternal = true
}
}
Key Concepts at Work
Actor Isolation:
- Ensures thread safety and serializes access to shared state (
isTaskDoneInternal
) throughGlobalManager
. @MainActor
guarantees main-thread access for UI-related properties and tasks.
- Ensures thread safety and serializes access to shared state (
SwiftUI Integration:
@Published
withObservableObject
enables reactive UI updates.
Encapsulation:
- Internal state (
isTaskDoneInternal
) is decoupled from the externally visible property (isTaskDone
).
- Internal state (
Concurrency-Safe Singleton:
- The combination of
@MainActor
,@GlobalManager
, andprivate init
creates a thread-safe singleton usable across the application.
- The combination of
ContentView
to integrate and provide visibility for this new Singleton.struct ContentView: View {
@StateObject private var locationManager = LocationManager.shared
@StateObject private var longTaskManager = LongTaskManager.shared
var body: some View {
VStack(spacing: 20) {
Text("LongTask is \(longTaskManager.isTaskDone ? "done" : "running...")")
...
.onAppear {
locationManager.checkAuthorization()
Task {
await longTaskManager.doLongTask()
}
}
.padding()
}
}
Key Concepts at Work
Singleton Reference:
Use a singleton reference to theLongTaskManager
.
The@StateObject
property wrapper ensures that any changes inLongTaskManager.isTaskDone
automatically update theContentView
.LongTaskManager Execution Status:
ThelongTaskManager.isTaskDone
property determines the message displayed based on the execution status.Start Long Task:
The.onAppear
modifier is the appropriate place to invokelongTaskManager.doLongTask()
.Testing on a Real Device:
Build and deploy the app on a real device (iPhone or iPad) to observe the long task execution. You’ll notice that it takes a while for the task to complete.

All the Singletons came together at one location
During app development, there may come a point where the number of Singletons in your project starts to grow uncontrollably, potentially leading to maintenance challenges and reduced code manageability. While Singletons offer advantages—such as providing centralized access to key functionality (e.g., Database, CoreLocation, AVFoundation)—they also have notable drawbacks:
- Global State Dependency: Code relying on a Singleton is often dependent on global state, which can lead to unexpected behaviors when the state is altered elsewhere in the application.
- Challenges in Unit Testing: Singletons retain their state across tests, making unit testing difficult and prone to side effects.
- Mocking Limitations: Replacing or resetting a Singleton for testing purposes can be cumbersome, requiring additional mechanisms to inject mock instances or reset state.
To address these challenges, the following Swift code defines a struct
named AppSingletons
. This struct serves as a container for managing singletons, simplifying dependency injection and promoting better application architecture.
import Foundation
struct AppSingletons {
var locationManager: LocationManager
var longTaskManager: LongTaskManager
init(locationManager: LocationManager = LocationManager.shared,
longTaskManager: LongTaskManager = LongTaskManager.shared) {
self.locationManager = locationManager
self.longTaskManager = longTaskManager
}
}
var appSingletons = AppSingletons()
Ensure that singleton references are obtained from appSinglegons
.
struct ContentView: View {
@StateObject private var locationManager = appSingletons.locationManager
@StateObject private var longTaskManager = appSingletons.longTaskManager
var body: some View {
After performing a sanity check to ensure everything is working, let’s move on to the test target and add the following unit test:
import Testing
@testable import GatherMultipleSingletons
struct GatherMultipleSingletonsTests {
@Test @MainActor func example() async throws {
let longTaskManagerMock = LongTaskManagerMock()
appSingletons = AppSingletons(longTaskManager: longTaskManagerMock)
#expect(appSingletons.longTaskManager.isTaskDone == false)
await appSingletons.longTaskManager.doLongTask()
#expect(appSingletons.longTaskManager.isTaskDone == true)
}
}
final class LongTaskManagerMock: LongTaskManager {
override func doLongTask() async {
await MainActor.run {
isTaskDone = true
}
}
}
AppSingleton
and LongTaskManager
, ensuring that the singleton’s behavior matches expectations under controlled test conditions. By using the mock, the test becomes predictable and faster, avoiding the need for actual long-running logic....Thread safe touch
Now is time to turn this code into a thread safe. Set Swift Concurrency Checking to Complete:

… and Swift language version to Swift 6.

The first issue we identified is that, from a non-isolated domain, the struct is attempting to access an isolated one (@MainActor). Additionally, appSingletons is not concurrency-safe because, as mentioned, it resides in a non-isolated domain.

ContentView (@MainActor) is currently accessing this structure directly. The best approach would be to move the structure to an @MainActor-isolated domain.
import Foundation
@MainActor
struct AppSingletons {
var locationManager: LocationManager
var longTaskManager: LongTaskManager
init(locationManager: LocationManager = LocationManager.shared,
longTaskManager: LongTaskManager = LongTaskManager.shared) {
self.locationManager = locationManager
self.longTaskManager = longTaskManager
}
}
@MainActor var appSingletons = AppSingletons()
This means that the LongTaskManager
is executed only within the @MainActor
. However, this isn’t entirely true. The part responsible for accessing shared attributes and updating the @Published
property is executed under the @MainActor
, but the part performing the heavy lifting runs in a @globalActor
isolated domain.
Conclusions
In this post I have showed a way avoid Singleton discontrol, by gathering them in a global structure. You can find the source code used in this post in the following repository.
References
- Migrating to Swift 6
Official Swift.org guide
- Migrate your app to Swift 6
WWDC 2024 video
- CoreLocation
Apple Developer Documentation
- Swift 6 migration recipes
JaviOS post