Если completion-блок хранится как свойство класса, должен ли он быть escaping?

Ответ

Да, такой блок должен быть помечен как @escaping.

По умолчанию в Swift параметры-функции являются non-escaping. Это означает, что они должны быть выполнены до завершения работы функции, в которую переданы. Если же блок сохраняется в свойство класса (или любой другой внешней переменной), его время жизни потенциально продлевается за рамки этой функции — он может быть вызван позже, асинхронно. Такой блок является escaping.

Пример, требующий @escaping:

class DataManager {
    var completionHandler: (() -> Void)? // Блок хранится как свойство

    func fetchData(completion: @escaping () -> Void) {
        // Сохраняем блок для вызова позже
        self.completionHandler = completion

        // Имитируем асинхронную операцию
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            // Вызываем блок после завершения функции `fetchData`
            completion()
        }
    }
}

Если убрать @escaping, компилятор выдаст ошибку: "Escaping closure captures non-escaping parameter 'completion'", так как блок «сбегает» за пределы области вызова функции.

Ответ 18+ 🔞

А, ну это ж классика, блядь! Прямо как в том анекдоте про Герасима и Муму, только про замыкания.

Смотри, в Свифте эти блоки кода, они по умолчанию — как немой Герасим, пока ты с ним в одной комнате. Пока функция работает, он должен всё своё дело сделать и нахуй свалить. Не может он, сука, остаться жить у тебя в квартире после того, как ты дверь закрыл. Это non-escaping, блядь.

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

И компилятор, этот бдящий пидорас шерстяной, сразу начинает орать: «Ты куда это, мудак, non-escaping параметр пытаешься в вечность отправить? Он же тут должен отработать и сдохнуть!». И будет прав, сука.

Вот смотри на пример, тут всё как на ладони:

class DataManager {
    var completionHandler: (() -> Void)? // Вот эта полка, куда мы Герасима сажаем

    func fetchData(completion: @escaping () -> Void) {
        // Сохраняем блок для вызова позже — вот он, момент побега!
        self.completionHandler = completion

        // Имитируем асинхронную операцию
        DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
            // Вызываем блок после того, как функция `fetchData` уже давно кончилась!
            completion() // А Герасим-то живой!
        }
    }
}

Видишь? Функция fetchData уже отработала, вернула управление, а этот ёбаный completion ещё сидит в очереди и ждёт своей секунды, чтобы выскочить и сказать «Муму!». Без пометки @escaping компилятор тебе мозги выест, потому что ты нарушаешь священный контракт, блядь.

Так что запомни: хочешь сохранить блок на потом — ставь @escaping. Не хочешь — не ставь, и пусть живёт только внутри функции. Всё просто, как ядрёна вошь.