@DTS Engineer Thanks for the explanation! That explains a lot but I guess two questions remain:
When we update a property of @Observable do we have to isolate to the main actor?
When we update the properties of an object that is a property of Observable do we have to isolate to the main actor?
Post
Replies
Boosts
Views
Activity
Thanks Quinn for chiming in. You are right, app architecture is not Apple's responsibility. I guess we just need to know the intended usage of the the API. So to that end, I guess the 'ask' is the following clarifications:
Are we suppose to update properties of a @Observable on the main thread?
Are we suppose to properties of an object that is a property of Observable on the main thread?
Is it safe to read properties of a @Observable on a background thread?
Is it safe to read properties of an object that is a property of @Observable on a background thread?
Is there a way to enforce check at compile time for setting properties of @Observable (for example by decorating it with @MainActor), but at the same time allow read to be not on the main thread?
I have a related question to this topic, I elaborated it in details here https://developer.apple.com/forums/thread/766805?page=1#810190022
@DTS Engineer / Quinn any chance we can get your help here :(
Now I've asked chatgpt if there is a hack we can do to alleviate this problem by making write on the mainthreads but allow read to be from any thread. It came up with this solution:
import Foundation
class UserModel {
private var _name: String
private var _age: Int
private let queue = DispatchQueue.main // Queue to ensure writes happen on the main thread
init(name: String, age: Int) {
self._name = name
self._age = age
}
// Custom getter allows read access from any thread
var name: String {
return _name
}
var age: Int {
return _age
}
// Custom setter ensures writes happen on the main thread
func update(name: String, age: Int) {
if Thread.isMainThread {
self._name = name
self._age = age
} else {
queue.async {
self._name = name
self._age = age
}
}
}
}
@Observable
class UserViewModel {
private var _userModel: UserModel
private let queue = DispatchQueue.main
init(userModel: UserModel) {
self._userModel = userModel
}
// Custom getter for the UserModel allows reads from any thread
var userModel: UserModel {
return _userModel
}
// Custom setter ensures writes happen on the main thread
func updateUserModel(name: String, age: Int) {
if Thread.isMainThread {
_userModel.update(name: name, age: age)
} else {
queue.async {
self._userModel.update(name: name, age: age)
}
}
}
}
// NetworkManager can now read from any thread and only needs to jump to the main thread for writes
class NetworkManager {
func updateUserInfo(viewModel: UserViewModel) {
Task {
// Read values from UserModel without jumping to the main thread
let userName = viewModel.userModel.name
let userAge = viewModel.userModel.age
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, update the UserModel on the main thread
viewModel.updateUserModel(name: "Jane Doe", 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)
}
Essentially wrapping each state with an internal backing and then controlling read and write. Does this approach have any issues? I dont trust chatgpt.
After some more investigation turns out this code crashes in preview because my Previews were ran in iOS17.
So I guess the question remains, why does this code works in iOS18 but not in iOS17 even though it doesn't use any iOS18 specific APIs?
If we have to target iOS17, what would be the solution here?
Does anyone have a workaround for this? I'm using Version 16.0 with iOS18 simulator, same error. So I cannot use transformer at all? Or is this a simulator issue and works on regular devices?
@DTS Engineer
Thanks for the tips, if thats the case, operations inside the viewModel should be mainactor isolated as well right? For example
@Observable
class SettingsViewModel {
var modelContainer: ModelContainer
...
@MainActor
func test() {
...
modelContainer.mainContext.insert(model)
modelContainer.mainContext.save()
}
accepted answer works, but pls fix xcode
@DTS Engineer
sorry I encounter another architecture question:
I have been studying ModelActor and I'm trying to create one so that I can do SwiftData operations in the background (say after a network call).
But according to this video https://www.youtube.com/watch?v=VG4oCnQ0bfw it seems the ModelActor itself needs to be created in the background off the main thread.
I want to create one actor to be used throughout the app, and not create a new actor for each background operation.
How could I do that? At first, a naive approach might be
import Foundation
actor ModelActor {
// Your ModelActor properties and methods
}
class ModelActorService {
static let shared = ModelActorService()
private(set) var modelActor: ModelActor?
private init() {
// Initialize on a background queue
DispatchQueue.global(qos: .background).async {
self.modelActor = ModelActor()
}
}
}
// ViewModel example
class SomeViewModel: ObservableObject {
private let modelActor: ModelActor
init(modelActor: ModelActor = ModelActorService.shared.modelActor!) {
self.modelActor = modelActor
}
// ViewModel methods using modelActor
}
but that won't work because the creation of the actor is async and there's no guarantee that it would actually be ready when the viewModel wants to use it.
How do I setup a actor facility that is global, created in the background, that can be used by various viewModels for background data operations?
@DTS Engineer thanks and I guess more a generic question, is ModelActor the intended approach to do SwfitData operations in the background? (like the scenario I outlined in the original post). Like am I on the right track?
@DTS Engineer thanks but can you confirm (3)
Do changes made in a background ModelActor trigger a icloud sync?
@DTS Engineer Thanks for the reply, some followup questions
With respect to (2):
It seems from the other thread that UI updates could be bugged still. But what is the intended behaviour when it eventually get fixed? Is @Query suppose to update from changes made from other modelContexts? And vice versa? Because if that is not the intended design, and our project requires it, we might be better off going back to core data.
With respect to (3):
Please comment. Is icloud sync suppose to trigger for changes made both in @Query modelContext and ModelActor's custom modelContext? (both pointed to the same container).
Is this something they will fix or we have to find workaround?
dont use it ever, it loops like crazy. Just use UIKit to dismiss, thats what I do