DispatchQueue.main.asyncAfter not delaying

9.1k Views Asked by At

My DispatchQueue.main.asyncAfter execution block does not wait to execute.

I wrote a MacOS single view app. (xCode 12.0.1 (12A7300)). It has a for-loop that calls a function that downloads content from my server. I want to throttle the requests. I am trying to use DispatchQueue.main.asyncAfter. But all the calls in the for-loop are made instantly, at the same time. Here is my code:

func fetchDocuments() {
    for index in 651...660 {
        let docNumber = String(index)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            print(Date())
            self.fetchDocument(byNumber: docNumber)
        }
    }
}

When I run this code I get this output on the console:

2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000

I am running this code from Xcode and observing the console.

Any help will be appreciated.

2

There are 2 best solutions below

1
On

'DispatchQueue.main.asyncAfter' is an asynchronous process. Here you have written'.now() + 2' for every statement. But your loop is executed very little time. So 2 seconds added with very little time for every statement inside the loop.

Try with this code below

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchDocuments()
    }

    func fetchDocuments() {
        var count = 0
        for index in 651...660 {
            let docNumber = String(index)
            DispatchQueue.main.asyncAfter(deadline: .now() + (Double(count+1)*2.0)) {
                print(Timestamp().printTimestamp())
                self.fetchDocument(byNumber: docNumber)
            }
            count += 1
        }
    }
    
    func fetchDocument(byNumber: String) {
        print("Hello World")
    }
}


class Timestamp {
    lazy var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS "
        return formatter
    }()

    func printTimestamp() {
        print(dateFormatter.string(from: Date()))
    }
}

Output:

2020-10-05 10:41:18.473
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
2
On

The call to asyncAfter returns immediately, which means that as you speed through your loop, all of these iterations are effectively firing 2 seconds from now, rather than two seconds from each other.

There are secondary issues that when you use asyncAfter, it’s a little tedious to cancel them if the object is deallocated and you want to stop the process. Also, if you schedule all of these asyncAfter up front, you will be subject to timer coalescing (which will manifest itself when latter scheduled events are within 10% of each other; that's not a problem with a range of 651...660, but could manifest itself if you used larger ranges).

A couple of common solutions include:

  1. A recursive pattern will ensure that each iteration fires two seconds after the prior one finishes:

    func fetchDocuments<T: Sequence>(in sequence: T) where T.Element == Int {
        guard let value = sequence.first(where: { _ in true }) else { return }
    
        let docNumber = String(value)
        fetchDocument(byNumber: docNumber)
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.fetchDocuments(in: sequence.dropFirst())
        }
    }
    

    And call it like so:

    fetchDocuments(in: 651...660)
    
  2. Another approach is to use a timer:

    func fetchDocuments<T: Sequence>(in sequence: T) where T.Element == Int {
        var documentNumbers = sequence.map { String($0) }
    
        let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
            guard
                let self = self,
                let documentNumber = documentNumbers.first
            else {
                timer.invalidate()
                return
            }
    
            self.fetchDocument(byNumber: documentNumber)
            documentNumbers.removeLast()
        }
        timer.fire() // if you don't want to wait 2 seconds for the first one to fire, go ahead and fire it manually
    }
    

Both of these (a) will provide two second interval between each call, (b) eliminate timer coalescing risks; and (c) will cancel if you dismiss the object in question.