The syntax is simply:
// to run something in 0.1 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// your code here
}
Note, the above syntax of adding seconds
as a Double
seems to be a source of confusion (esp since we were accustomed to adding nsec). That “add seconds as Double
” syntax works because deadline
is a DispatchTime
and, behind the scenes, there is a +
operator that will take a Double
and add that many seconds to the DispatchTime
:
public func +(time: DispatchTime, seconds: Double) -> DispatchTime
But, if you really want to add an integer number of msec, μs, or nsec to the DispatchTime
, you can also add a DispatchTimeInterval
to a DispatchTime
. That means you can do:
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
// 500 msec, i.e. 0.5 seconds
…
}
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(1_000_000)) {
// 1m microseconds, i.e. 1 second
…
}
DispatchQueue.main.asyncAfter(deadline: .now() + .nanoseconds(1_500_000_000)) {
// 1.5b nanoseconds, i.e. 1.5 seconds
…
}
These all seamlessly work because of this separate overload method for the +
operator in the DispatchTime
class.
public func +(time: DispatchTime, interval: DispatchTimeInterval) -> DispatchTime
It was asked how one goes about canceling a dispatched task. To do this, use DispatchWorkItem
. For example, this starts a task that will fire in five seconds, or if the view controller is dismissed and deallocated, its deinit
will cancel the task:
class ViewController: UIViewController {
private var item: DispatchWorkItem?
override func viewDidLoad() {
super.viewDidLoad()
item = DispatchWorkItem { [weak self] in
self?.doSomething()
self?.item = nil
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: item!)
}
deinit {
item?.cancel()
}
func doSomething() { … }
}
Note the use of the [weak self]
capture list in the DispatchWorkItem
. This is essential to avoid a strong reference cycle. Also note that this does not do a preemptive cancelation, but rather just stops the task from starting if it hasn’t already. But if it has already started by the time it encounters the cancel()
call, the block will finish its execution (unless you’re manually checking isCancelled
inside the block).
Swift concurrency
While the original question was about the old GCD dispatch_after
vs. the newer asyncAfter
API, this raises the question of how to achieve the same behavior in the newer Swift concurrency and its async
-await
. As of iOS 16 and macOS 13 we would prefer Task.sleep(for:)
:
try await Task.sleep(for: .seconds(2)) // 2 seconds
…
Or
try await Task.sleep(for: .milliseconds(200)) // 0.2 seconds
…
Or if we need to support back to iOS 13 and macOS 10.15, we would use Task.sleep(nanoseconds:)
instead.
And to support cancelation, save a Task
:
class ViewController: UIViewController {
private var task: Task<Void, Error>?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
task = Task {
try await Task.sleep(for: .seconds(5))
await doSomething()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
task?.cancel()
}
func doSomething() async { … }
}
Or in SwiftUI, we might use the .task {…}
view modifier, which “will automatically cancel the task at some point after the view disappears before the action completes.” We do not even need to save the Task
to manually cancel later.
We should recognize that before we had Swift concurrency, calling any sleep
function used to be an anti-pattern, one that we would studiously avoid, because it would block the current thread. That was a serious error when done from the main thread, but even was problematic when used in background threads as the GCD worker thread pool is so limited. But the new Task.sleep
functions do not block the current thread, and are therefore safe to use from any actor (including the main actor).
UInt64
? – Muire