skip to Main Content

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()
    }
}

4

Answers


  1. 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
                }
            }
        }
    
    Login or Signup to reply.
  2. 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)")
        }
    
    Login or Signup to reply.
  3. 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)
        }
    }
    
    Login or Signup to reply.
  4. 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")
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search