Reputation: 105
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
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
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
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
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