I've been obsessed with this topic for the past couple of weeks and unfortunately there just isn't a good answer out there even from the community. Therefore I am hoping that I can summon Quinn to get an official Apple position (on what's seemingly a fairly fundamental part of using SwiftUI).
Consider this simple example:
import Foundation
@MainActor
@Observable
class UserViewModel {
var name: String = "John Doe"
var age: Int = 30
// other properties and logic
}
// NetworkManager does not need to update the UI but needs to read/write from UserViewModel.
class NetworkManager {
func updateUserInfo(viewModel: UserViewModel) {
Task {
// Read values from UserViewModel prior to making a network call
let userName: String
let userAge: Int
// Even for a simple read, we have to jump onto the main thread
await MainActor.run {
userName = viewModel.name
userAge = viewModel.age
}
// Now perform network call with the retrieved values
print("Making network call with userName: \(userName) and userAge: \(userAge)")
// Simulate network delay
try await Task.sleep(nanoseconds: 1_000_000_000)
// After the network call, we update the values, again on the main thread
await MainActor.run {
viewModel.name = "Jane Doe"
viewModel.age = 31
}
}
}
}
// Example usage
let viewModel = UserViewModel()
let networkManager = NetworkManager()
// Calling from some background thread or task
Task {
await networkManager.updateUserInfo(viewModel: viewModel)
}
In this example, we can see a few things
- The ViewModel is a class that manages states centrally
- It needs to be marked as MainActor to ensure that updating of the states is done on the main thread (this is similar to updating @Published in the old days). I know this isn't officially documented in Apple's documentation. But I've seen this mentioned many times to be recommended approach including www.youtub_.com/watch?v=4dQOnNYjO58 and here also I have observed crashes myself when I don't follow this practise
Now so far so good, IF we assume that ViewModel are only in service to Views. The problem comes when the states need to be accessed outside of Views.
in this example, NetworkManager is some random background code that also needs to read/write centralized states. In this case it becomes extremely cumbersome. You'd have to jump to mainthread for each write (which ok - maybe that's not often) but you'd also have to do that for every read.
Now. it gets even more cumbersome if the VM holds a state that is a model object, mentioned in this thread..
Consider this example (which I think is what @Stokestack is referring to)
import Foundation
// UserModel represents the user information
@MainActor // Ensuring the model's properties are accessed from the main thread
class UserModel {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
@MainActor
@Observable
class UserViewModel {
var userModel: UserModel
init(userModel: UserModel) {
self.userModel = userModel
}
}
// NetworkManager does not need to update the UI but needs to read/write UserModel inside UserViewModel.
class NetworkManager {
func updateUserInfo(viewModel: UserViewModel) {
Task {
// Read values from UserModel before making a network call
let userName: String
let userAge: Int
// Jumping to the main thread to safely read UserModel properties
await MainActor.run {
userName = viewModel.userModel.name
userAge = viewModel.userModel.age
}
// Simulate a network call
print("Making network call with userName: \(userName) and userAge: \(userAge)")
try await Task.sleep(nanoseconds: 1_000_000_000)
// After the network call, updating UserModel (again, on the main thread)
await MainActor.run {
viewModel.userModel.name = "Jane Doe"
viewModel.userModel.age = 31
}
}
}
}
// Example usage
let userModel = UserModel(name: "John Doe", age: 30)
let viewModel = UserViewModel(userModel: userModel)
let networkManager = NetworkManager()
// Calling from a background thread
Task {
await networkManager.updateUserInfo(viewModel: viewModel)
}
Now I'm not sure the problem he is referring still exists (because I've tried and indeed you can make codeable/decodables marked as @Mainactor) but it's really messy.
Also, I use SwiftData and I have to imagine that @Model basically marks the class as @MainActor for these reasons.
And finally, what is the official Apple's recommended approach? Clearly Apple created @Observable to hold states of some kind that drives UI. But how do you work with this state in the background?