I have the following view model:
@MainActor
final class ProfileViewModel: ObservableObject {
let authService: AuthService
@Published var userHandle: String = "Unknown"
nonisolated init(authService: AuthService, appState: AppState) {
self.authService = authService
appState.$userData
.receive(on: DispatchQueue.main)
.map {
$0.user?.displayName ?? "Unknown"
}
.assign(to: &$userHandle)
}
}
Which results in error: "Main actor-isolated property '$userHandle' cannot be used 'inout' from a non-isolated context", at the ".assign(to:)" line.
This is somewhat expected. As I understand it, Combine's dynamic thread switching doesn't satisfy compile-time guarantees of Swift's concurrency model.
Here's what's puzzling me. If I remove the @MainActor annotation from the class and apply it just to the userHandle property, like this:
final class ProfileViewModel: ObservableObject {
let authService: AuthService
@MainActor @Published var userHandle: String = "Unknown"
nonisolated init(authService: AuthService, appState: AppState) {
self.authService = authService
appState.$userData
.receive(on: DispatchQueue.main)
.map {
$0.user?.displayName ?? "Unknown"
}
.assign(to: &$userHandle)
}
}
The code compiles without a problem.
Logically, this should raise the same error, since userHandle is Main actor-isolated in both cases. I was wondering if this is a compiler quirk or if there's a deeper semantic difference that I don't get.
Dispatch and Swift Concurrency is incompatible. During compile time, the compiler can only make a best effort to figure out whether code executes on the main thread when Dispatch is involved.
Anyway, I would suggest you refactor your class ProfileViewModel
so that the class as a whole becomes MainActor isolated. Optionally, you may check whether you actually execute on the MainActor:
@MainActor
final class ProfileViewModel: ObservableObject {
let authService: AuthService
@Published var userHandle: String = "Unknown"
var cancellable: AnyCancellable!
init(authService: AuthService, appState: AppState) {
self.authService = authService
cancellable = appState.$userData
.receive(on: DispatchQueue.main)
.map { $0 }
.sink(receiveValue: { string in
MainActor.assumeIsolated { // <== Optional runtime check
self.userHandle = string
}
})
}
}
Note, that when you change the queue to some other non-main queue, the compiler will not issue any errors or warnings!
So you might want to add this check at runtime:
MainActor.assumeIsolated { ... }
when your code executes within a dispatch block.