Магия DispatchQueue.main.asyncAfter: как устроена задержка в Swift 3

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему «подождать» — это не так просто, как кажется

Представьте: вы пишете приложение для iOS на Swift 3. Нужно показать анимацию, а через пару секунд — скрыть её. Или отправить запрос на сервер, но не сразу, а с задержкой. Казалось бы, что может быть проще? Берёшь и пишешь sleep(2) — и готово. Но если вы так сделаете, ваше приложение превратится в ледышку: интерфейс зависнет, пользователь начнёт тыкать экран, а в итоге — негативный отзыв и потеря рейтинга.

Вопрос «How to program a delay in Swift 3» набрал тысячи просмотров на Stack Overflow. И это не случайно: новички постоянно наступают на одни и те же грабли. В этой статье мы не просто дадим готовый рецепт. Мы залезем под капот механизма задержек, разберёмся, как работает DispatchQueue.main.asyncAfter, и поймём, почему sleep — это зло. А ещё я покажу вам лайфхак, который сэкономит часы дебага.

Почему sleep() — антипаттерн в Swift

Давайте сразу разберёмся с самым популярным заблуждением. Многие, кто приходит из других языков (например, из Python с его time.sleep()), пытаются сделать так:

sleep(2) // блокируем поток на 2 секунды
print("Прошло 2 секунды")

Этот код блокирует текущий поток. Если вы вызвали его на главном потоке (main thread) — всё, приложение «засыпает» на 2 секунды. Пользовательский интерфейс не обновляется, кнопки не нажимаются, анимации не работают. iOS просто убьёт ваше приложение, если главный поток будет заблокирован слишком долго (обычно больше 10 секунд).

Кроме того, sleep() не учитывает состояние RunLoop. Это грубый системный вызов, который не даёт системе нормально обрабатывать события. В Swift 3 (и далее) правильный путь — использовать Grand Central Dispatch (GCD).

DispatchQueue.main.asyncAfter: правильный способ сделать задержку

Самый распространённый и рекомендуемый способ отложить выполнение кода в Swift 3 — это метод asyncAfter из GCD. Вот как он выглядит:

DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
    print("Прошло 2 секунды (неблокирующий режим)")
}

Здесь мы говорим системе: «Через 2 секунды от текущего момента выполни этот блок кода на главном потоке». Главный поток при этом остаётся свободным — анимации работают, кнопки нажимаются, пользователь счастлив.

Но давайте копнём глубже. Что такое deadline? Почему .now() + 2.0, а не просто 2.0? И что происходит «под капотом»?

Анатомия asyncAfter: deadline, DispatchTime и DispatchQoS

asyncAfter принимает параметр deadline типа DispatchTime. Это не просто число миллисекунд, а абсолютная точка во времени, привязанная к системным часам. Когда вы пишете .now() + 2.0, GCD вычисляет: «текущее время + 2 секунды» и ставит задачу в очередь с этой меткой.

Под капотом GCD использует mach_absolute_time() — аппаратный таймер, который не зависит от изменений системного времени. То есть если пользователь переведёт часы назад, ваша задержка не сломается (в отличие от Timer, который может повести себя непредсказуемо).

Важный нюанс: deadline — это минимальное время до выполнения. GCD не гарантирует, что код выполнится ровно через 2 секунды. Если система занята (например, идёт сложная обработка данных), выполнение может задержаться. Для большинства UI-задач это не критично, но для высокоточных таймеров (например, в играх) лучше использовать CADisplayLink.

Ещё один параметр — qos (Quality of Service). Вы можете указать приоритет задачи:

DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1.0) {
    // задача с высоким приоритетом
}

Это влияет на то, как система распределяет ресурсы между потоками. Если не указывать, используется значение по умолчанию для очереди.

DispatchWorkItem: отмена задержки

Часто возникает ситуация: вы запланировали задержку, но потом передумали. Например, пользователь нажал кнопку «Отмена». Как отменить уже запланированное выполнение? Тут на помощь приходит DispatchWorkItem.

Вот как это работает:

// Создаём работу
let workItem = DispatchWorkItem {
    print("Это сообщение может быть отменено")
}

// Планируем через 3 секунды
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: workItem)

// Если нужно отменить — вызываем cancel()
workItem.cancel()

Когда вы вызываете cancel(), GCD просто не запускает блок, даже если время уже настало. Но есть хитрость: если работа уже начала выполняться, cancel() её не прервёт. Он только выставляет флаг isCancelled. Внутри блока вы можете проверить этот флаг:

let workItem = DispatchWorkItem {
    if workItem.isCancelled { return }
    print("Работа выполнена")
}

Это особенно полезно, когда вы работаете с замыканиями, которые могут «устареть» (например, при загрузке данных с сервера, когда пользователь уже ушёл с экрана).

Timer: альтернатива с повторением

Иногда вам нужно не просто сделать задержку, а запустить повторяющееся действие. Например, обновлять счётчик каждую секунду. Для этого в Swift 3 есть Timer.

var timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    print("Тик")
}

// Чтобы остановить через 5 секунд:
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
    timer.invalidate()
}

Обратите внимание: Timer работает на RunLoop. Если вы создали таймер в фоновом потоке, он не запустится, пока вы не запустите RunLoop вручную. Для главного потока это происходит автоматически. Если вам нужен таймер, который не зависит от RunLoop (например, для фоновой синхронизации), используйте DispatchSourceTimer — это более низкоуровневый, но и более мощный инструмент.

DispatchSourceTimer: промышленный уровень

Когда обычного Timer недостаточно (например, нужна высокая точность или таймер должен работать в фоне), используйте DispatchSourceTimer:

let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer.schedule(deadline: .now(), repeating:                

Блоги

Рекомендаций по книгам