Reputation: 926
I would like to understand the proper usage of Task { }
in the following SwiftUI code. My goal is to get a fundamental understand to avoid memory leaks.
This is the sample code for the SwiftUI part:
struct MyView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text(viewModel.publishedString)
}
.onAppear(perform: {
Task {
viewModel.resetUI()
await viewModel.doSomeAsyncStuff()
}
})
.onDisappear {
Task {
await viewModel.doAnotherAsyncStuff()
viewModel.resetUI()
}
}
}
}
The .onAppear { … }
and .onDisappear { … }
modifier will call a synchronous and asynchronous function of the ViewModel class. The Text(viewModel.publishedString)
is representing a published property (using @Published
) that is modified by viewModel.doSomeAsyncStuff()
and viewModel.doAnotherAsyncStuff()
.
Keep in mind that I need to be backwards-compatible to SwiftUI 2.0, which is why I am not using the .task { }
modifier.
Can you please confirm, if my understanding is correct with regards to memory leaks and retain cycles for this example:
Task { … }
, a copy of the MyView type value will be created inside the Task closureViewModel
classViewModel
is not allocating new memory (because it is just a pointer, whereas the copy of struct will allocating new memory that is freed up upon completion of the Task closurepublishedString
property inside the Task closure will update the SwiftUI view because all copies of the View struct holding the same reference to the ViewModel class.onAppear { … }
and .onDisappear { … }
) are completed, all allocated memory for View struct and ViewModel class references are freed up.Upvotes: 3
Views: 2560
Reputation: 30746
@StateObject
is designed to remove the need for using onAppear
. The object is init before the view appears the first time and deinit when it disappears. So you could start your task inside the object's init
and cancel it in deinit
. Note, we usually call this object a "Loader" and not a "View Model", because in SwiftUI the View
struct plus @State
is already the view model.
If you want to run your task every time it appears instead of just the first time then you can add a restart func
to cancel and start the task again, and call that from onAppear
.
Update: I was thinking about this more and I believe it is possible to store the Task
handle in a @State
thus reimplementing something like .task
:
struct TaskTestView: View {
@State var task: Task<Void, Never>?
var body: some View {
Text("Test")
.onAppear {
task = Task {
try? await Task.sleep(for: .seconds(3))
if Task.isCancelled {
print("cancelled")
return
}
print("complete")
}
}
.onDissapear {
task?.cancel()
}
}
}
Test with this:
struct TaskTestView2: View {
@State var showing = false
var body: some View {
VStack {
if (showing) {
TaskTestView()
}
else {
Text("Not showing")
}
}
Button("Show/Hide") {
showing.toggle()
}
}
}
Upvotes: 4