Introduction
Swift 6 introduces strict concurrency checking to help developers identify and fix data races at compile time. Overlapping access to shared mutable state can lead to crashes, misbehavior, or corrupted user data. Actors provide compile-time and runtime isolation guarantees, ensuring safe access to actor-isolated state.
However, there is currently no mechanism to automatically propagate changes from @Observable state to an actor that depends on it. Developers must explicitly push updates manually, which introduces boilerplate and potential risks for stale state or incorrect concurrent access.
This pre-proposal introduces @ActorSynced, an attribute to automatically synchronize observable state into actor-isolated properties, eliminating boilerplate and strengthening concurrency safety in Swift 6 applications.
Motivation
Consider a common scenario in apps: managing an authentication token across UI and background services.
@Observable
struct AuthState {
var jwtToken: String? = nil
}
A @MainActor-bound session manager updates this token:
@MainActor
@Observable
final class AuthSessionManager {
var authState: AuthState
init(authState: AuthState) {
self.authState = authState
}
func login(token: String) {
authState.jwtToken = token
Task { await NetworkService.shared.updateToken(token) } // explicit bridging
}
func logout() {
authState.jwtToken = nil
Task { await NetworkService.shared.updateToken(nil) }
}
}
Meanwhile, NetworkService executes on background threads:
actor NetworkService {
static let shared = NetworkService()
private var jwtToken: String?
func updateToken(_ newToken: String?) {
jwtToken = newToken
}
func authorizedRequest(to url: URL) -> URLRequest {
var request = URLRequest(url: URL(string: "https://api.example.com")!)
if let token = jwtToken {
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return request
}
}
Currently, developers must explicitly propagate changes into the actor, which is error-prone. Missing an update can lead to stale state, incorrect requests, or subtle concurrency bugs.
Swift 6’s strict concurrency objectives aim to prevent overlapping access to mutable state. Introducing automatic synchronization between observables and actors would align perfectly with these objectives by ensuring safe, consistent updates across main and background actors.
Runtime Considerations
Synchronizing an observable property into an actor may involve runtime mechanisms such as task scheduling, priority handling, and isolation enforcement. For example:
-
Updates from the main actor to a background actor must respect task priorities to avoid starvation or inconsistent state.
-
The actor model ensures that updates are serialized, preventing data races even in highly concurrent contexts.
These runtime guarantees, combined with Swift 6’s compile-time concurrency checks, strengthen the reliability and correctness of concurrent apps.
Proposed Solution
Introduce an attribute @ActorSynced to automatically propagate changes from an @Observable property into an actor-isolated property.
Option 1: Actor-side declaration
actor NetworkService {
@ActorSynced(\AuthState.jwtToken)
private var jwtToken: String?
}
Option 2: Observable-side declaration
@Observable
struct AuthState {
@SyncAcross(NetworkService.shared)
var jwtToken: String? = nil
}
The compiler/runtime would automatically:
-
Serialize updates to the actor property.
-
Ensure actor isolation is preserved.
-
Remove the need for manual
Task { await … }calls or.onChangeobservers.
Benefits
-
Eliminates boilerplate bridging code.
-
Reduces risk of stale or unsynchronized state.
-
Improved run time scheduling of tasks in an actor (say priority inversion)
-
Preserves SwiftUI’s declarative semantics.
-
Ensures strictly safe concurrent access across actors executing on main and background threads.
-
Aligns with Swift 6’s actor/message-passing concurrency philosophy, conceptually closer to Erlang/Scala than C++ shared-memory threads.
Alternatives Considered
-
.onChangeobservers: works but verbose and error-prone. -
didSet/ manualTaskpushes: repetitive and easy to forget. -
Passing snapshots into actors: breaks live updates.
Discussion Points
-
Should
@ActorSyncedbe declared on the actor or on the observable? -
Behaviour for suspended actors and actors that are no longer available.
-
Could this leverage Swift macros or attributes to generate bridging code safely?