Singleton
A Singleton gives one shared instance to a process. iOS APIs such as URLSession.shared and UserDefaults.standard make the idea familiar, but a shared instance also creates shared state and implicit dependencies.
When it helps in iOS
Use a shared instance when the app truly needs one coordinator for a process-wide resource, such as a single in-memory session snapshot. Avoid using it simply to make a service accessible from every view.
Example: a process-wide session snapshot
import Foundation
@MainActor
final class SessionSnapshot {
static let shared = SessionSnapshot()
private init() {}
private(set) var accountID: String?
func signIn(accountID: String) {
self.accountID = accountID
}
func signOut() {
accountID = nil
}
}
// At the app boundary, after authentication succeeds:
@MainActor
func recordSignIn(accountID: String) {
SessionSnapshot.shared.signIn(accountID: accountID)
}The private initializer prevents other code from making another SessionSnapshot. @MainActor keeps reads and writes to this UI-facing snapshot on one actor. It is an in-memory view of session state, not secure credential storage.
How to use it in a real app
Pass the shared instance through an initializer or SwiftUI environment at the app root. To substitute it in previews or tests, make consumers depend on a small session protocol and inject a fake implementation there. Store tokens in a suitable secure store; do not put secrets in this snapshot.
Watch for: Global mutable state makes tests interfere and hides ownership. If several scenes or accounts need independent sessions, a Singleton is the wrong lifetime. Create one session per scene or account and inject it.
Trace the lifetime
static let shared initializes the instance once on first access. The private initializer prevents accidental second instances. @MainActor gives UI-facing access one isolation domain, but it does not make the data persistent across launches. Sign-out must clear the snapshot explicitly.
A useful test boundary is a small SessionReading protocol with an accountID property. Features can receive that protocol while the app injects SessionSnapshot.shared. This keeps the singleton at the composition root. Avoid a singleton for per-scene navigation, per-document editing, or multiple signed-in accounts because those have different lifetimes.
Distinguish identity from access
static let shared provides one SessionSnapshot instance in the process, and the private initializer prevents accidental second instances. @MainActor defines how UI-facing code accesses its mutable accountID; it does not persist state or encrypt anything. signIn and signOut are explicit transitions. The global access point is convenient, but code that calls it directly has an invisible dependency on account state.
From example to an iOS app
At app startup, create services and pass the shared snapshot into a session-reading protocol at the composition root. A profile view model can read account identity through that protocol while a test supplies a fake. After successful authentication, update the snapshot on the main actor. Store credentials in a secure credential store through a separate service. On sign-out, clear the snapshot and invalidate account-scoped caches. If the app supports several windows with different accounts, one process-wide snapshot cannot represent them correctly.
Failure modes and verification
Test sign-in, sign-out, and account switching, including whether old feature state disappears. Avoid putting unrelated services into a giant AppManager.shared. A singleton fixes lifetime and identity globally; Dependency Injection makes dependencies visible to each consumer. You can use a singleton at the app boundary and still inject an interface into features.