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?