Probably the most commonly repeated iOS programming question I see on Stack Overflow is some variation on the problem of what it means for code to be asynchronous. The issue is often exacerbated when the questioner doesn’t even know that a piece of code is asynchronous.
So, what’s asynchronous code? It’s a chunk of code that runs out of order with respect to the surrounding code. To be precise, it runs later than the surrounding code. The usual use case is networking, but we can illustrate quite satisfactorily with a case of delayed performance.
This is some actual code from a recent Stack Overflow question:
@IBAction func answerPressed(_ sender: UIButton) {
self.view.isUserInteractionEnabled = false
score = score + 1
scoreLabel.text = "Score: \(score)"
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
self.questionNumber = self.questionNumber + 1
self.updateQuestion()
})
self.view.isUserInteractionEnabled = true
}
What is this code supposed to do? The programmer wants to bring everything to a pause for two seconds while the new score is displayed. During that two seconds, nothing should happen, and the user of the app should be unable to tap on anything — hence the isUserInteractionEnabled = false
. After two seconds have gone by, the interface should change to show the next question, and the user should now be able to tap again (isUserInteractionEnabled = true
).
But there’s a problem: the programmer has discovered that the user can tap during the two second pause. Why is that?
To understand the programmer’s misconception, let’s look at how he thinks the code goes. He thinks the code runs in this order:
@IBAction func answerPressed(_ sender: UIButton) {
// 1
self.view.isUserInteractionEnabled = false
// 2
score = score + 1
// 3
scoreLabel.text = "Score: \(score)"
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
// 4
self.questionNumber = self.questionNumber + 1
// 5
self.updateQuestion()
})
// 6
self.view.isUserInteractionEnabled = true
}
That’s a natural mistake! The code runs down the page one line at a time, so the path of code execution should run down the page one line at a time too, right? Wrong!
The code inside the asyncAfter
block is asynchronous. That’s the whole point of asyncAfter
: it delays the running of its code. So the actual order of execution is like this:
@IBAction func answerPressed(_ sender: UIButton) {
// 1
self.view.isUserInteractionEnabled = false
// 2
score = score + 1
// 3
scoreLabel.text = "Score: \(score)"
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
// 5! (two seconds later than 4)
self.questionNumber = self.questionNumber + 1
// 6!
self.updateQuestion()
})
// 4!
self.view.isUserInteractionEnabled = true
}
So what happens when the code runs? The code (1) sets isUserInteractionEnabled = false
, (2) increments and (3) displays the score, and (4) immediately sets isUserInteractionEnabled = true
. So user interaction was never disabled for any appreciable time at all! It’s as if user interaction was never disabled.
What the programmer wanted was for the re-enabling of user interaction to be itself delayed along with the display of the new question, until after the two seconds had elapse. So he should have put the re-enabling of user interaction as part of the asynchronous block, like this:
@IBAction func answerPressed(_ sender: UIButton) {
// 1
self.view.isUserInteractionEnabled = false
// 2
score = score + 1
// 3
scoreLabel.text = "Score: \(score)"
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
// 4, two seconds later
self.view.isUserInteractionEnabled = true
// 5
self.questionNumber = self.questionNumber + 1
// 6
self.updateQuestion()
})
}
And that’s the big mistake people make with asynchronous code. They put some code physically after the asynchronous block, thinking that that means the code will run temporally after the asynchronous block. But it doesn’t mean that! "Asynchronous" means out of time. The asynchronous block runs later than its surroundings. Code physically after the asynchronous block will run together with the code physically before the asynchronous block. The asynchronous block does not bring the surrounding code to a halt. The surrounding code does not wait.
So now you do understand what "asynchronous" means.
You might want to go on reading, about using a value after it has been set by asynchronous code.