Sometimes, you want some code to execute after a specified delay. For me, this happens often in user interfaces; there are many cases where I want some notification or other interface element to appear and then disappear after a couple of seconds. I used to use an NSTimer
to make it happen, but nowadays, I call on a simple method called delay()
.
Consider the simple “Magic 8-Ball” app design shown below:
The two functional interface items are the Tap me button and a label that displays a random “yes/no/maybe” answer in response to a button tap. The button should be disabled and the answer should remain onscreen for three seconds, after which the app should revert to its initial state, with the button enabled and the answer label blank.
Here’s the action method that responds to the Touch Up Inside event on the “Tap me” button:
@IBAction func buttonClicked(sender: UIButton) {
tapMeButton.enabled = false
predictionLabel.text = randomAnswer()
delay(3.0) {
self.tapMeButton.enabled = true
self.predictionLabel.text = ""
}
}
Don’t worry too much about the randomAnswer()
method; it simply returns a randomly-selected string from an array of possible answers. The really interesting method is delay()
, which takes two parameters:
- A number of seconds that the system should wait before executing a block of code. In this particular case, we want a 3-second delay.
- The block of code to be executed after the delay. In our block, we want to blank the label and enable the button.
The block of code that we’re passing to delay()
is a closure, which means it will be executed outside the current ViewController
object, which in turns means that we’ve got to be explicit when capturing variables in the current scope. We can’t just refer to the button and label as tapMeButton
and predictionLabel
, but by their fully-qualified names, self.tapMeButton and self.predictionLabel
.
Here’s the code for delay()
:
func delay(delay: Double, closure: ()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
closure
)
}
delay()
is just a wrapper for dispatch_after()
, one of the functions in Grand Central Dispatch, Apple’s library for running concurrent code on multicore processors on iOS and OS X. dispatch_after()
takes three parameters:
- How long the delay should be before executing the block of code should be,
- the queue on which the block of code should be run, and
- the block of code to run.
We could’ve simply used dispatch_after()
, but it exposes a lot of complexity that we don’t need to deal with. Matt Neuburg, the author of the O’Reilly book iOS 9 Programming Fundamentals with Swift, found that he was using dispatch_after()
so often that he wrote delay()
as a wrapper to simplify his code. Which would you rather read — this…
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(3.0 * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
{
self.tapMeButton.enabled = true
self.predictionLabel.text = ""
}
)
…or this?
delay(3.0) {
self.tapMeButton.enabled = true
self.predictionLabel.text = ""
}
Here’s the code for the example “Magic 8-Ball” app, which I’ve put entirely in the view controller for simplicity’s sake:
//
// ViewController.swift
//
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tapMeButton: UIButton!
@IBOutlet weak var predictionLabel: UILabel!
let predictions = [
"Yes.",
"Sure thing.",
"But of course!",
"I'd bet on it.",
"AWWW YISSS!",
"No.",
"Nuh-uh.",
"Absolutely not!",
"I wouldn't bet on it.",
"HELL NO.",
"Maybe.",
"Possibly...",
"Ask again later.",
"I can't be certain.",
"Reply hazy. Try again later."
]
override func viewDidLoad() {
super.viewDidLoad()
predictionLabel.text = ""
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func buttonClicked(sender: UIButton) {
// When the "Tap me" button tapped,
// we want to:
// 1. Disable the button
// 2. Display a random magic 8-ball answer
// 3. Wait 3 seconds, and then:
// a) Enable the button
// b) Clear the displayed answer
tapMeButton.enabled = false
predictionLabel.text = randomAnswer()
delay(3.0) {
self.tapMeButton.enabled = true
self.predictionLabel.text = ""
}
}
func randomAnswer() -> String {
return predictions[randomIntUpToButNotIncluding(predictions.count)]
}
}
// MARK: Utility functions
func delay(delay: Double, closure: ()->()) {
// A handy bit of code created by Matt Neuburg, author of a lot of books including
// iOS Programming Fundamentals with Swift (O'Reilly 2015).
// See his reply in Stack Overflow for details:
// http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861
//
// The secret sauce is Grand Central Dispatch's (GCD) dispatch_after() function.
// Ray Wenderlich has a good tutorial on GCD at:
// http://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(),
closure
)
}
func randomIntUpToButNotIncluding(count: Int) -> Int {
return Int(arc4random_uniform(UInt32(count)))
}
Resources
You can see this code in action by downloading the zipped project files for the demo project, DelayDemo [220K Xcode 7 / Swift 2 project and associated files, zipped].
If you’d like to learn more about coding for concurrency with Grand Central Dispatch, a good starting place is the tutorial on Ray Wenderlich’s site. It’s a two parter; here’s part 1, and here’s part 2.
I found Matt Neuburg’s delay()
method in his answer to this Stack Overflow question.