Paul Mielle
Paul Mielle

Reputation: 105

Task cancellation in SwiftUI

I have come across a strange behavior (or at least one I don't understand) while trying to cancel a Task. Here is a minimal example: I have a Task that sleeps 30 seconds and then increment a counter.

However, if I call .cancel() on that Task before 30 seconds have passed then the counter is incremented immediately.

I would have expected that cancelling the Task would not increment the counter value; does anyone have an idea of what is going on here?

Thank you!

import SwiftUI

struct ContentView: View {
    @State var task: Task<Void, Never>? = nil  // reference to the task
    @State var counter = 0
    
    var body: some View {
        VStack(spacing: 50) {
            
            // display counter value and spawn the Task
            Text("counter is \(self.counter)")
                .onAppear {
                    self.task = Task {
                        try? await Task.sleep(nanoseconds: 30_000_000_000)
                        self.counter += 1
                    }
                }

            // cancel button
            Button("cancel") {
                self.task?.cancel()  // <-- when tapped before 30s, counter value increases. Why?
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 4

Views: 6018

Answers (4)

malhal
malhal

Reputation: 30746

In SwiftUI it's best to use the .task modifier to use async/await when you want the task lifetime tied to what's on screen, e.g.

@State var isStarted = false
...
Button(isStarted ? "Stop" : "Start") {
    isStarted.toggle()
}
.task(id: isStarted) { // runs on appear, cancels on disappear, restarted on id changes.
    if !isStarted {
        return
    }
    do {
        try await Task.sleep(for: .seconds(3))
        counter += 1
    } catch {
        print("Cancelled") // on disappear and on the stop button.
    }
    isStarted = false // if it was stopped using the button this will already be false.
}

Upvotes: 4

tanmoy
tanmoy

Reputation: 1438

Placing the code inside a do catch block is enough.

self.task = Task {
    do {
        try await Task.sleep(nanoseconds: 3_000_000_000)
        self.counter += 1
    } catch {
        print(error)
    }
}

Upvotes: 1

Joakim Danielson
Joakim Danielson

Reputation: 52043

When a task is canceled an error is thrown but you are ignoring the thrown error by using try?

Here is a variant of your code that will react properly to the cancellation

self.task = Task {
    do {
        try await Task.sleep(nanoseconds: 30_000_000_000)
        self.counter += 1
    } catch is CancellationError {
        print("Task was cancelled")
    } catch {
        print("ooops! \(error)")
    }

Upvotes: 6

DevB2F
DevB2F

Reputation: 5095

How about adding a check for isCancelled like this:

Text("counter is \(self.counter)")
    .onAppear {
        print("onappear..")
        self.task = Task {
            try? await Task.sleep(nanoseconds: 30_000_000_000)
            if !self.task!.isCancelled {
                self.counter += 1
            }
        }
    }

Upvotes: 0

Related Questions