In my SwiftUI application i rely on live data, which means i got a data model with @Published annotated data that I need to update the UI with. The UI is updated using a ObservableObject ViewModel class, that i want to annotate with @MainActor. Right now the code looks like this:
// Model
class TestModel {
@Published var data: Any = ... // Updated regularly
static let sharedInstance = TestModel()
}
// ViewModel
class TextViewModel: ObservableObject {
@Published var data: Any = ...
final var disposables: Set<AnyCancellable> = []
init() {
TestModel.sharedInstance.$data
.receive(on: DispatchQueue.main)
.sink {
self.data = $0
}.store(in: &disposables)
}
}
This currently works correctly, but i would like to move to the new @MainActor + async await methodology. My first thought would be to do something like this:
// ViewModel
@MainActor
class TextViewModel: ObservableObject {
@Published var data: Any = ...
init() {
Task {
for await data in TestModel.sharedInstance.$data.values {
self.data = data
}
}
}
}
So my questions are, is this a valid way of handling "infinite" values? Will this Task get cancelled, when the TextViewModel is deinitialized? Same for the memory of that Task, will it be freed on deinit?
Since im fairly new to Swift + SwiftUI any tips would be much appreciated!
The documentation for the assign(to:) operator states:
Use this operator when you want to receive elements from a publisher and republish them through a property marked with the @Published attribute. The assign(to:) operator manages the life cycle of the subscription, canceling the subscription automatically when the Published instance deinitializes. Because of this, the assign(to:) operator doesn’t return an AnyCancellable that you’re responsible for like assign(to:on:) does.
I think you are right with that. Should those memory leaks be shown by the memory leak checker in instruments? Or are reference cycles not detected? Because those are not shown for me.
Yes. TextViewModel.init() runs on the main actor, and the Task { … } you launch from there inherits this actor context. This ensures that your @Published property is updated on the main thread.
No and no. The Task is a totally independent task. It will run to completion unless explicitly cancelled, and its lifetime is managed by the concurrency runtime.
You should store the task in a property inside the view model and then cancel it manually in deinit. (You might encounter a complication here due to Deinit and MainActor, I'm not sure. It's a good idea to activate the concurrency checks with -Xfrontend -warn-concurrency.)