The Magic of DispatchQueue.main.asyncAfter: How Delay Works in Swift 3

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: why "waiting" is not as simple as it seems

\n\n

Imagine: you are writing an iOS app in Swift 3. You need to show an animation, and after a couple of seconds — hide it. Or send a request to the server, but not immediately, with a delay. It would seem, what could be simpler? You just write sleep(2) — and it's done. But if you do that, your app will turn into a block of ice: the interface will freeze, the user will start tapping the screen, and in the end — a negative review and a loss of rating.

\n\n

The question "How to program a delay in Swift 3" has thousands of views on Stack Overflow. And this is no coincidence: beginners constantly step on the same rake. In this article, we will not just give a ready-made recipe. We will dig under the hood of the delay mechanism, understand how DispatchQueue.main.asyncAfter works, and understand why sleep is evil. And I will also show you a life hack that will save hours of debugging.

\n\n

Why sleep() is an anti-pattern in Swift

\n\n

Let's immediately deal with the most popular misconception. Many who come from other languages (for example, from Python with its time.sleep()) try to do this:

\n\n
sleep(2) // block the thread for 2 seconds\nprint("2 seconds have passed")
\n\n

This code blocks the current thread. If you call it on the main thread — that's it, the app "falls asleep" for 2 seconds. The user interface does not update, buttons are not pressed, animations do not work. iOS will simply kill your app if the main thread is blocked for too long (usually more than 10 seconds).

\n\n

In addition, sleep() does not take into account the state of the RunLoop. This is a crude system call that does not allow the system to process events normally. In Swift 3 (and later), the correct way is to use Grand Central Dispatch (GCD).

\n\n

DispatchQueue.main.asyncAfter: the correct way to make a delay

\n\n

The most common and recommended way to delay code execution in Swift 3 is the asyncAfter method from GCD. Here is what it looks like:

\n\n
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {\n    print("2 seconds have passed (non-blocking mode)")\n}
\n\n

Here we tell the system: "In 2 seconds from the current moment, execute this block of code on the main thread." The main thread remains free — animations work, buttons are pressed, the user is happy.

\n\n

But let's dig deeper. What is deadline? Why .now() + 2.0 and not just 2.0? And what happens "under the hood"?

\n\n

Anatomy of asyncAfter: deadline, DispatchTime, and DispatchQoS

\n\n

asyncAfter takes a deadline parameter of type DispatchTime. This is not just a number of milliseconds, but an absolute point in time tied to the system clock. When you write .now() + 2.0, GCD calculates: "current time + 2 seconds" and places the task in the queue with this timestamp.

\n\n

Under the hood, GCD uses mach_absolute_time() — a hardware timer that does not depend on changes in system time. That is, if the user turns the clock back, your delay will not break (unlike Timer, which can behave unpredictably).

\n\n

An important nuance: deadline is the minimum time until execution. GCD does not guarantee that the code will execute exactly after 2 seconds. If the system is busy (for example, complex data processing is underway), execution may be delayed. For most UI tasks, this is not critical, but for high-precision timers (for example, in games), it is better to use CADisplayLink.

\n\n

Another parameter is qos (Quality of Service). You can specify the priority of the task:

\n\n
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1.0) {\n    // high priority task\n}
\n\n

Blogs

Book Recommendations