Как перестать бояться задержек и полюбить GCD в Swift 3, 4 и 5
Вы когда-нибудь писали код, который должен был выполниться через три секунды, но срабатывал мгновенно? Или, наоборот, ждали целую вечность, хотя планировали пару миллисекунд? Знакомо? Тогда вы точно сталкивались с dispatch_after — одним из самых популярных, но при этом самых коварных инструментов Grand Central Dispatch (GCD).
Раньше, в эпоху Swift 2 и Objective-C, этот метод выглядел как заклинание: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(nanoseconds)), dispatch_get_main_queue()). Но с приходом Swift 3, 4 и 5 API GCD полностью переписали. Теперь это не просто смена названия — это новый способ мышления. В этой статье мы разберем, как писать dispatch_after в Swift на всех актуальных версиях, развеем мифы и дадим готовые решения. Поехали!
Что случилось с dispatch_after? Краткая история боли
До Swift 3 разработчики использовали C-стиль GCD:
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Double(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
print("Привет из прошлого!")
}
Это работало, но выглядело громоздко. В Swift 3 Apple решила "свифтизировать" GCD, превратив его в объектно-ориентированный API. Теперь dispatch_after превратился в метод asyncAfter у структуры DispatchQueue. И это не просто косметический ремонт — это полностью новый синтаксис, который требует понимания.
Основной рецепт: asyncAfter в Swift 3, 4 и 5
Независимо от версии Swift (3, 4 или 5), базовый синтаксис dispatch_after теперь выглядит одинаково. Вот самый простой пример отложенного выполнения на главном потоке:
let delayInSeconds: Double = 2.0
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) {
print("Прошло 2 секунды!")
}
Ключевые моменты:
- DispatchQueue.main — основной поток (UI).
- .now() + delayInSeconds — создаем deadline (крайний срок) относительно текущего момента.
- asyncAfter — асинхронное выполнение после указанного времени.
Этот код работает в Swift 3.0, 4.0 и 5.0 без изменений. Магия в том, что Apple оставила этот API стабильным после 2016 года.
А как же dispatch_time? Работа с наносекундами
Иногда вам нужно указать задержку в наносекундах (например, для точных таймеров или игровой логики). В новом API используется DispatchTime:
let nanoseconds: Int = 1_500_000_000 // 1.5 секунды
let deadline = DispatchTime.now() + .nanoseconds(nanoseconds)
DispatchQueue.global().asyncAfter(deadline: deadline) {
print("Выполнено через 1.5 секунды в фоновом потоке")
}
Вы также можете использовать готовые константы: .seconds(5), .milliseconds(500) и даже .microseconds(1000). Это удобнее, чем умножать на NSEC_PER_SEC.
Частые вопросы (и ответы!) про dispatch_after в Swift
На Stack Overflow этот вопрос собрал тысячи просмотров. Вот самые популярные грабли, на которые наступают новички.
1. Почему мой код выполняется немедленно, а не с задержкой?
Самая распространенная ошибка — путаница между asyncAfter и async. Если вы забыли указать deadline: или написали DispatchQueue.main.async { ... }, код выполнится сразу. Вторая причина: передача отрицательного значения. .now() + (-5.0) — это прошедшее время, GCD выполнит блок немедленно.
2. Как отменить dispatch_after?
В отличие от Timer, у asyncAfter нет встроенного метода cancel(). Но есть два решения:
- Флаг отмены: Используйте
DispatchWorkItem. - Таймер: Если нужна отменяемая задержка, используйте
Timer.scheduledTimer(withTimeInterval:repeats:block:).
Пример с DispatchWorkItem:
let workItem = DispatchWorkItem {
print("Можно отменить!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)
// Если передумали:
workItem.cancel()
3. Работает ли dispatch_after в SwiftUI или Combine?
Да, GCD — это низкоуровневый API, который работает везде. Но если вы используете SwiftUI, рассмотрите более "свифтовый" подход: Task.sleep(nanoseconds:) из async/await (Swift 5.5+). Вот пример:
func delayedTask() async {
do {
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 секунды
print("Это async/await!")
} catch {
print("Задача прервана")
}
}
Однако для обратной совместимости (iOS 12 и ниже) dispatch_after через GCD остается лучшим выбором.
4. Как сделать повторяющуюся задержку (таймер)?
Многие пытаются вызывать asyncAfter рекурсивно. Это работает, но лучше использовать DispatchSourceTimer или Timer:
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
timer.schedule(deadline: .now(), repeating: .seconds(5))
timer.setEventHandler {
print("Повторение каждые 5 секунд")
}
timer.activate()
Миграция с Objective-C на Swift: пошаговое руководство
Если вы переносите старый код на Objective-C, вот таблица соответствия:
| Objective-C (старый) | Swift 3/4/5 (новый) |
|---|---|
dispatch_after(..., dispatch_get_main_queue()) |
DispatchQueue.main.asyncAfter(...) |
dispatch_time(DISPATCH_TIME_NOW, ...) |
DispatchTime.now() + .seconds(...) |
dispatch_get_global_queue(0, 0) |
DispatchQueue.global() |
Совет: не пытайтесь смешивать старый C-стиль с новым Swift-стилем в одном проекте — это приведет к путанице.
Практический пример: анимация с задержкой
Давайте напишем