Achim
Achim

Reputation: 229

SwiftUI DispatchQueue asyncAfter stops working correctly after ten scheduled tasks

If I run the code below, it counts correctly to 10 and then skips every odd number.

If you let it run beyond 20, it will start skipping more and more (the output will be like 1-2-3-4-5-6-7-8-9-10-12-14-16-18-20-23-26-29 ...).

Couldn't find documentation of e.g. a limit of scheduled tasks, so asking you :) Learning SwiftUI since yesterday, so pardon if it's an obvious question.

If it's scheduled in a different DispatchQueue than main, it makes no difference.

Thanks for helping!

import SwiftUI

struct ContentView: View {
    @State private var counter : Int = 0
    
    func buttonTap() {
        let time : DispatchTime = .now()
        
        var delay : Double = 0
        
        for _ in 1...50 {
            
            delay = delay + 0.2
            
            DispatchQueue.main.asyncAfter(deadline: time + delay) {
                counter += 1
            }
            
        }
    }
    
    var body: some View {
        ZStack {
            
            Color(.black)
                .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
            
            Text(String(counter))
                .foregroundColor(.green)
                .padding()
            
            Text("Button")
                .foregroundColor(.green)
                .offset(y: 50)
                .onTapGesture(perform: {
                    buttonTap()
                })
            
        }
    }
}

Upvotes: 0

Views: 974

Answers (2)

Rob
Rob

Reputation: 437632

This behavior is known as timer coalescing, where independently scheduled events that occur within 10% of each other will be coalesced together, and will be run at the same time. This is a power saving feature.

There are a variety of solutions to avoid coalescing:

  • use a repeating timer;
  • schedule each iteration in the completion handler of the prior iteration; or
  • create “strict” GCD timers that won’t be subject to coalescing.

The repeating timer is the most common solution.

Upvotes: 1

vacawama
vacawama

Reputation: 154603

That is an odd behavior, and it even occurs for much longer time intervals between events (for example, 2 seconds).

Consider using a Timer instead:

You can use multiple single fire timers that work just like your separate dispatches:

func buttonTap() {
    
    var delay : Double = 0
    
    for _ in 1...50 {
        
        delay = delay + 0.2
        
        Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { timer in
            self.counter += 1
        }
        
    }
}

or a single timer that fires every 0.2 seconds:

func buttonTap() {
    Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { timer in
        self.counter += 1
        if counter == 50 {
            timer.invalidate()
        }
    }
}

Upvotes: 1

Related Questions