Maschina
Maschina

Reputation: 926

Proper usage of Task { } in SwiftUI

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:

  1. During call of Task { … }, a copy of the MyView type value will be created inside the Task closure
  2. The copy of that struct type value is holding a reference to the ViewModel class
  3. The hold reference to the ViewModel 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 closure
  4. Function calls to the ViewModel class and updates of the published publishedString 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
  5. Completing the Task closure will de-allocate the copy of the View struct
  6. If the initial MyView value (not the copy inside the Task closure) has to be de-allocated from memory because it is not needed anymore, but the Task closure has not been completed yet, a strong reference is hold to the ViewModel reference only until completion of Task closure. As soon as the Task closures (in .onAppear { … } and .onDisappear { … }) are completed, all allocated memory for View struct and ViewModel class references are freed up.

Upvotes: 3

Views: 2560

Answers (1)

malhal
malhal

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

Related Questions