«Подожди секунду»: почему задержки в Swift — это магия, а не хаки
Представьте: вы пишете приложение для заказа пиццы. Пользователь нажимает «Оплатить», и мгновенно — бам! — списание денег. Без анимации, без паузы, без чувства «что-то происходит». Пользователь пугается, закрывает приложение, а вы теряете клиента. Задержка в Swift — это не костыль, а инструмент UX-дизайна и правильной архитектуры.
Вопрос «How to create a delay in Swift?» на Stack Overflow собрал десятки тысяч просмотров. И неудивительно: новички пытаются использовать sleep() (спойлер: так нельзя), а профи ищут элегантные решения. Мы разберём 5 рабочих способов — от классического GCD до современных async/await. Поехали!
1. Классика: DispatchQueue.main.asyncAfter (iOS 4+)
Самый популярный способ создания задержки в Swift — это DispatchQueue.main.asyncAfter. Он работает на любой версии iOS и не требует дополнительных импортов. Подходит для выполнения кода на главном потоке (например, обновление UI).
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Задержка 2 секунды
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("Прошло 2 секунды!")
self.showAlert()
}
}
func showAlert() {
let alert = UIAlertController(title: "Привет!", message: "Это задержка", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Плюсы и минусы
- Плюсы: простота, обратная совместимость, работает на главном потоке.
- Минусы: нельзя отменить задержку (если не сохранить DispatchWorkItem).
2. Отменяемая задержка через DispatchWorkItem
Иногда нужно отменить задержку — например, пользователь ушёл с экрана. DispatchWorkItem решает эту проблему. Сохраняем ссылку на «работу» и вызываем cancel().
class DelayedTask {
private var workItem: DispatchWorkItem?
func scheduleDelayedAction() {
// Создаём задачу
let item = DispatchWorkItem { [weak self] in
print("Задача выполнена после задержки")
self?.doSomething()
}
// Сохраняем и запускаем
workItem = item
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: item)
}
func cancelDelayedAction() {
workItem?.cancel()
workItem = nil
print("Задача отменена")
}
func doSomething() {
// Ваш код
}
}
// Использование
let task = DelayedTask()
task.scheduleDelayedAction()
// Через 1 секунду отменяем
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
task.cancelDelayedAction()
}
Обратите внимание на [weak self] — это обязательно, чтобы избежать утечек памяти в Swift. Задержка может пережить контроллер, если не ослабить ссылку.
3. Timer.scheduledTimer: задержка с повторением
Таймеры Swift — это не только про повторяющиеся задачи. Можно создать одноразовый таймер с задержкой. Плюс: таймер легко отменяется и может быть приостановлен.
class TimerExample {
var timer: Timer?
func startDelayedAction() {
// Одноразовый таймер через 1.5 секунды
timer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { [weak self] _ in
print("Таймер сработал через 1.5 сек")
self?.updateUI()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
func updateUI() {
// Обновляем интерфейс
}
}
Когда использовать Timer
- Нужно отменить задержку в любой момент.
- Задержка может превратиться в периодическое действие.
- Работа с анимациями (например, задержка перед скрытием элемента).
4. Современный Swift: async/await и Task.sleep (iOS 13+)
Начиная с iOS 13, Swift поддерживает асинхронность через async/await. Task.sleep — это самый «свифтовый» способ создать задержку. Код читается как обычный синхронный, но не блокирует поток.
import SwiftUI
struct ContentView: View {
@State private var message = "Ожидание..."
var body: some View {
Text(message)
.font(.largeTitle)
.task {
await simulateNetworkRequest()
}
}
func simulateNetworkRequest() async {
print("Начинаем задержку...")
// Задержка 3 секунды (Task.sleep принимает наносекунды!)
try? await Task.sleep(nanoseconds: 3_000_000_000)
// Обновляем UI (главный поток гарантирован)
message = "Готово! Прошло 3 секунды"
print("Задержка завершена")
}
}
Важно: Task.sleep использует наносекунды. Чтобы не ошибаться, используйте константы:
let seconds = 2.0
try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
Почему это лучше GCD?
- Чистый, линейный код без вложенных замыканий.
- Автоматическая обработка отмены (через Task.isCancelled).
- Безопасная работа с UI (SwiftUI автоматически переключает поток).
5. Combine: задержка в реактивном стиле (iOS 13+)
Если вы используете Combine для обработки событий, delay(for:scheduler:) — ваш выбор. Позволяет отложить поток данных на определённое время.
import Combine
class CombineViewModel: ObservableObject {
@Published var searchText = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .seconds(0.5), scheduler: RunLoop.main) // Задержка 0.5 сек
.sink { [weak self] text in
print("Поиск после паузы: \(text)")
self?.performSearch(text)
}
.store(in: &cancellables)
}
func performSearch(_ query: String) {
// Реальный поисковый запрос
}
}
Этот пример — классический «debounce» для поискового поля. Задержка помогает избежать лишних запросов, пока пользователь печатает.
Чего делать НЕ стоит: sleep() и RunLoop
Золотое правило iOS-разработки: никогда не вызывайте Thread.sleep(forTimeInterval:) или sleep() на главном потоке. Это блокирует UI, приложение «зависает», и пользователь думает, что оно с