I am trying to execute a series of tasks with a delay. To experiment that, I created a simple app with two labels and a button to perform hit test.
Upon hitting the "Hit me" button, yellow label must update the count instantly, where the red label must update the same count with a delay of 3 seconds. Below is my code.
class ViewController: UIViewController {
@IBOutlet weak var hitMeButton: UIButton!
@IBOutlet weak var actualCountLabel: UILabel!
@IBOutlet weak var processedCountLabel: UILabel!
private let delay: UInt32 = 3
private var queuedHits: [Int] = [] {
didSet {
for hit in self.queuedHits {
// First item to be updated instantly as the queue might be applied only to the items behind
self.processedHits.append(hit)
// Applying delay to the iteration for the rest of items
sleep(self.delay)
}
self.queuedHits.removeAll()
}
}
private var actualHits: [Int] = [] {
didSet {
self.actualCountLabel.text = "(self.actualHits.count)"
if let last = self.actualHits.last {
self.queuedHits.append(last)
}
}
}
private var processedHits: [Int] = [] {
didSet {
self.processedCountLabel.text = "(self.processedHits.count)"
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupInitialValues()
}
private func setupInitialValues() {
actualCountLabel.text = "0"
processedCountLabel.text = "0"
}
@IBAction func didHitButton(_ sender: UIButton) {
actualHits.append(1)
}
}
Here I use sleep()
function to delay the execution. When I perform the hit test, I see that the entire app is frozen and both the labels update their count after 3 seconds. sleep()
function does not satisfy my need here. What would be the proper way to do it?
My need is to iterate through the queuedHits
with a delay and without blocking the main thread.
2
Answers
you can use the DispatchQueue "asyncAfter" class to schedule the updates with a delay
private let delay: TimeInterval = 3
Using "sleep" in the main thread can block the UI and make your app Hang
You should avoid traditional
sleep
calls, as those block the calling thread. While you could considerasyncAfter
, that can run into timer coalescing problems.I would suggest using a
Timer
. The trick is that because we are no longer blocking, you need to be prepared forqueuedHits
to be mutated while the previous iteration may still be underway. So, if previous iteration is not complete, let the previous timer simply proceed. Only if there is no existing timer would you create a new one. And, needless to say, when there are no morequeuedHits
, you would stop the timer. E.g.:It, admittedly, can sometimes be hard to reason about asynchronous processes, so we often reach for
async
–await
. Specifically, we might use forAsyncChannel
of Apple’s Swift Async Algorithms.So we set up a routine to monitor the channel for new values and then
Task.sleep
(which, unlike legacysleep
functions, does not block the thread):And when the view appears, we would start monitoring the channel, and when it disappears, cancel that task:
So, pulling that together: