skip to Main Content

In the view, there are two buttons that display a text prompt when clicked, and the display of the text is controlled by the @State var tipText: String?. I have written an asynchronous function using the .task(id:priority:_:) modifier that shows the text when the user clicks a button, and hides it automatically after three seconds.

struct SwiftUIView: View {
    @State var tipText: String?

    var body: some View {
        ZStack {
            VStack {
                HStack {
                    Button("Red") {
                        tipText = "Red"
                    }
                    .tint(.red)
                    .controlSize(.large)
                    .buttonStyle(.borderedProminent)

                    Button("Blue") {
                        tipText = "Blue"
                    }
                    .tint(.blue)
                    .controlSize(.large)
                    .buttonStyle(.borderedProminent)
                }

                Spacer()
            }
            .padding()

            // Shows the text when the user clicks a button
            if let tipText = tipText {
                Text(
                    """
                    You have just clicked the (tipText) button,
                    this prompt will disappear in 3 seconds.
                    """
                )
                .padding()
                .background(.ultraThickMaterial)
                .cornerRadius(10)

                // Close the text prompt after three seconds.
                .task(id: tipText) {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        self.tipText = nil
                    }
                }
            }
        }
    }
}

However, if the user quickly clicks back and forth between the two buttons, the text will disappear early. I suspect that this is because the earlier execution of the asynchronous function caused the text to be hidden prematurely.

Here you can see the effect of the code execution.

My expectation is that regardless of how many times the user clicks continuously, the text must wait for three seconds after I stop clicking before disappearing. Is there a better way to implement this?

2

Answers


  1. The new Concurency does not mix with DispatchQueue.

    Replace

    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    
    }
    

    With

    try? await Task.sleep(for: .seconds(3))
    self.tipText = nil
    

    https://developer.apple.com/documentation/swift/task/sleep(for🙂

    Or the nanosecond version that is available for earlier versions.

    https://developer.apple.com/documentation/swift/task/sleep(nanoseconds🙂

    Login or Signup to reply.
  2. .task is called on appear and is cancelled/restarted every time the id param changes so you need logic to handle those cases. Also you’ll need a counter e.g.

    @State var clickCount = 0
    
    Button("Red") {
        tipText = "Red"
        clickCount += 1
    }
    
    Button("Blue") {
        tipText = "Blue"
        clickCount += 1
    }
    
    .task(id: clickCount) {
        if clickCount == 0 { // return early the first time
            return
        }
        do {
            try await Task.sleep(for: .seconds(3)) // exceptions if cancelled because id changed while sleeping.
            tipText = nil
        }
        catch {
            // was cancelled
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search