Какие проблемы могут возникнуть при использовании DispatchQueue.main.asyncAfter и как их избежать?

Ответ

DispatchQueue.main.asyncAfter — полезный метод для отложенного выполнения задач, но с ним связаны несколько типичных проблем:

1. Циклы удержания (Retain Cycles): Замыкание захватывает self сильной ссылкой, удерживая его до своего выполнения, даже если self больше не нужен.

Неправильно (потенциальный цикл):

class MyViewController {
    func scheduleUpdate() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.updateUI() // Сильный захват self
        }
    }
}

Правильно (используем weak):

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
    guard let self = self else { return } // Проверяем, что объект жив
    self.updateUI()
}

2. Накопление задач: Повторные вызовы могут привести к накоплению незавершенных задач, если предыдущая еще не выполнилась.

Решение — использование DispatchWorkItem с отменой:

class TaskScheduler {
    private var pendingWorkItem: DispatchWorkItem?

    func scheduleTask() {
        // Отменяем предыдущую задачу, если она есть
        pendingWorkItem?.cancel()

        let workItem = DispatchWorkItem { [weak self] in
            guard let self = self else { return }
            // Выполняемый код
            self.handleDelayedAction()
        }
        pendingWorkItem = workItem
        DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem)
    }
}

3. Проверка потока: Хотя DispatchQueue.main.asyncAfter гарантирует выполнение на главном потоке, код внутри замыкания должен быть готов к этому. Если задача была отменена (DispatchWorkItem.cancel()), замыкание не будет выполнено.

Ответ 18+ 🔞

А, слушай, этот ваш DispatchQueue.main.asyncAfter — штука, вроде, простая, как валенок, но столько народу на ней обжигается, что пиздец просто. Вроде бы отложил задачу на две секунды и пошёл пить чай, а потом оказывается, что твой вьюконтроллер висит в памяти, как призрак, и ни хуя не выгружается. Волнение ебать!

Вот смотри, главная подстава номер раз — это циклы удержания. Замыкание-то это, сука, жадное. Оно хватает self и не отпускает, будто репейник к штанине.

class MyViewController {
    func scheduleUpdate() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.updateUI() // Вот тут, блядь, и ловушка! Сильный захват!
        }
    }
}

Сделал так — и всё, пиши пропало. Объект будет жить, пока таймер не сработает, даже если ты уже десять раз назад на экран нажал. Чистая утечка, ёпта.

Как надо, чтобы не было мучительно больно? Да ослабить ссылку, блядь!

DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
    guard let self = self else { return } // Проверили, жив ли ещё наш страдалец
    self.updateUI()
}

Вот так — аккуратно, с проверкой. Если вьюконтроллера уже нет, просто выходим и никого не держим. Красота.


А вторая беда — накопление задач. Представь: у тебя кнопка, и на каждый тап ты ставишь отложенную задачу. Пользователь тыкает, как сумасшедший, пять раз подряд. И что? А потом через две секунды твой хендлер вызовется тоже пять раз подряд, и интерфейс, блядь, сломается в хлам. Удивление пиздец!

Тут надо быть хитрожопым и уметь отменять предыдущие поручения. Спасает DispatchWorkItem.

class TaskScheduler {
    private var pendingWorkItem: DispatchWorkItem? // Храним последнюю задачу

    func scheduleTask() {
        // Первым делом — отменяем то, что висело в воздухе
        pendingWorkItem?.cancel()

        let workItem = DispatchWorkItem { [weak self] in
            guard let self = self else { return }
            // А вот тут уже делаем что нужно
            self.handleDelayedAction()
        }
        pendingWorkItem = workItem // Запоминаем новую
        DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem)
    }
}

Вот так — старую задачу нахуй выкинули, новую поставили. Теперь только последний вызов сработает. Умно, да?


И последнее, на что глаз положи — проверка потока. Сам asyncAfter выполнит код на главной очереди, это да. Но если ты внутри делаешь что-то, что с главным потоком не дружит (ну, какой-нибудь тяжёлый расчёт), то получишь лаги и фризы. И ещё: если задачу отменили через cancel(), замыкание просто не запустится. Никаких уведомлений, никаких ошибок — тишина, блядь. Имей в виду.

Короче, инструмент мощный, но острый. С ним надо аккуратно, а не как слон в посудной лавке.