Search code examples
iosswiftgrand-central-dispatchfor-in-loop

Using delay DispatchQueue in "for in loop"


The task is to change the background color once a second. Was used "for in loop". For delay, a DispatchQueue was used. Everything seems to be fine, but it was noticed that after 10 iterations, the background color begins to change with a delay of 2 seconds, a little later in 3 seconds. The more iterations, the greater the delay. I displayed time in the console (seconds) to see how it changes. I see the results, but I do not understand what is wrong. I did the task through a timer, there were no problems, but I want to understand what is wrong with the DispatchQueue delay.

for i in 1...150 {

    DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {

        self.view.backgroundColor = UIColor(red: .random(in: 0...1),
                                          green: .random(in: 0...1), 
                                           blue: .random(in: 0...1), 
                                          alpha: 1)
         print("\(ymd)")

         ymd = self.myCalendar.dateComponents([.second], from: Date())
    }
}

Console:

second: 21 isLeapMonth: false 
second: 21 isLeapMonth: false 
second: 22 isLeapMonth: false 
second: 23 isLeapMonth: false 
second: 24 isLeapMonth: false 
second: 25 isLeapMonth: false 
second: 26 isLeapMonth: false 
second: 27 isLeapMonth: false 
second: 28 isLeapMonth: false 
second: 29 isLeapMonth: false 
second: 30 isLeapMonth: false 
second: 32 isLeapMonth: false 
second: 33 isLeapMonth: false 
second: 33 isLeapMonth: false 
second: 35 isLeapMonth: false 
second: 35 isLeapMonth: false 
second: 37 isLeapMonth: false 
second: 37 isLeapMonth: false 
second: 39 isLeapMonth: false 
second: 39 isLeapMonth: false 
second: 41 isLeapMonth: false 
second: 41 isLeapMonth: false 
second: 44 isLeapMonth: false 
second: 44 isLeapMonth: false 
second: 44 isLeapMonth: false 
second: 47 isLeapMonth: false 
second: 47 isLeapMonth: false 
second: 47 isLeapMonth: false 
second: 50 isLeapMonth: false 
second: 50 isLeapMonth: false 
second: 50 isLeapMonth: false 
second: 54 isLeapMonth: false 
second: 54 isLeapMonth: false 
second: 54 isLeapMonth: false 
second: 57 isLeapMonth: false 
second: 57 isLeapMonth: false 
second: 57 isLeapMonth: false 
second: 57 isLeapMonth: false 
second: 1 isLeapMonth: false 
second: 1 isLeapMonth: false 
second: 1 isLeapMonth: false 
second: 1 isLeapMonth: false 
second: 6 isLeapMonth: false 
second: 6 isLeapMonth: false 
second: 6 isLeapMonth: false 
second: 6 isLeapMonth: false 
second: 6 isLeapMonth: false 
second: 11 isLeapMonth: false 
second: 11 isLeapMonth: false 
second: 11 isLeapMonth: false 
second: 11 isLeapMonth: false 
second: 11 isLeapMonth: false 
second: 17 isLeapMonth: false 
second: 17 isLeapMonth: false 
second: 17 isLeapMonth: false 
second: 17 isLeapMonth: false 
second: 17 isLeapMonth: false 

Solution

  • This is a result of “timer coalescing”, in which the “leeway” for the dispatched block is 10% of the delay, up to a max of one minute of leeway. (This is buried in the libdispatch code.) It’s a power saving feature to coalesce/group distant, independently scheduled tasks to run at the same time to avoid unnecessary spinning up the hardware too many times. The easiest way to avoid this coalescing is to use a repeating timer:

    var counter = 0
    
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
        counter += 1
    
        guard let self = self, counter <= 150 else {
            timer.invalidate()
            return
        }
    
        self.view.backgroundColor = UIColor(red: .random(in: 0...1),
                                          green: .random(in: 0...1),
                                           blue: .random(in: 0...1),
                                          alpha: 1)
    
        let ymd = self.myCalendar.dateComponents([.second], from: Date())
        print(ymd)
    }
    

    Note the use of the [weak self] pattern, to avoid having the timer keep a persistent reference to the view controller. And with that guard statement, we’ll invalidate the timer if the view controller is dismissed.

    BTW, if you want to make the color change less jarring, animate the change:

    var counter = 0
    
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
        counter += 1
        guard let self = self, counter <= 150 else {
            timer.invalidate()
            return
        }
    
        UIView.animate(withDuration: 0.1) {
            self.view.backgroundColor = UIColor(red: .random(in: 0...1),
                                              green: .random(in: 0...1),
                                               blue: .random(in: 0...1),
                                              alpha: 1)
        }
    
        ...
    }