Замыкания в iOS повсюду: completion-обработчики, анимации, подписки, сортировка. На собесе обязательно спросят про захват, @escaping и [weak self].
Синтаксис
let sorted = names.sorted(by: { (a: String, b: String) -> Bool in return a < b })
let sorted2 = names.sorted { $0 < $1 } // вывод типов, неявный return, trailing closure
let sorted3 = names.sorted(by: <) // оператор это функция
Если последних аргументов-замыканий несколько, с Swift 5.3 их пишут подряд: UIView.animate(withDuration: 0.3) { ... } completion: { _ in ... }.
Захват переменных
Замыкание захватывает переменную, а не её значение: переменная переезжает в бокс в куче, и замыкание видит последнее значение и может его менять.
var x = 10
let byRef = { print(x) }
let byValue = { [x] in print(x) } // список захвата: копия в момент создания
x = 20
byRef() // 20
byValue() // 10
Классический пример: счётчик.
func makeCounter() -> () -> Int {
var n = 0
return { n += 1; return n }
}
let a = makeCounter()
a(); a() // 1, 2
let b = a
b() // 3: замыкания ссылочные, состояние общее
В цикле for-in каждая итерация создаёт новую константу, поэтому замыкания, созданные в цикле, увидят свои значения. Если же все замыкания захватили одну внешнюю var, изменяемую в while, они увидят её финальное значение.
escaping и non-escaping
Замыкание-параметр по умолчанию non-escaping: оно гарантированно не переживёт вызов функции. Компилятору это позволяет держать контекст на стеке и не требовать явного self.
Если замыкание сохраняется в свойство или вызывается асинхронно, нужен @escaping:
final class Loader {
private var handlers: [(Data) -> Void] = []
func load(completion: @escaping (Data) -> Void) {
handlers.append(completion)
}
}
Опциональное замыкание (() -> Void)? всегда escaping, атрибут к нему не пишется.
Ограничение: escaping-замыкание не может захватить inout-параметр, а значит и self внутри mutating-метода структуры.
self и циклы ссылок
В escaping-замыкании внутри класса self нужно писать явно: это напоминание, что вы захватываете сильную ссылку. Если объект хранит замыкание, а замыкание держит объект, получается цикл, и ARC их не освободит.
final class ProfileViewModel {
var onUpdate: (() -> Void)?
func bind() {
api.subscribe { [weak self] profile in
guard let self else { return }
self.profile = profile
self.onUpdate?()
}
}
}
[weak self]: замыкание не удерживает объект,selfстановится опциональным.[unowned self]: без опционала, но крэш, если объект уже освобождён. Только когда замыкание точно не переживёт объект.
Когда weak не нужен: в non-escaping замыканиях (map, filter, UIView.animate) и в однократных асинхронных вызовах, где замыкание не хранится объектом (DispatchQueue.main.async). Там жизнь self лишь немного продлевается.
Ловушка с lazy: lazy var handler = { self.doWork() } создаёт цикл, потому что замыкание хранится в свойстве объекта.
@autoclosure
Оборачивает выражение-аргумент в замыкание, которое вычисляется по требованию:
func log(_ enabled: Bool, _ message: @autoclosure () -> String) {
if enabled { print(message()) }
}
log(false, heavyDescription()) // heavyDescription не вызовется
Так устроены assert, precondition, правые части ?? и &&.
@Sendable
Замыкание, которое может выполниться в другом домене конкурентности, должно быть @Sendable: оно захватывает только Sendable-значения и не мутирует захваченные переменные. Подробнее в модуле про Swift Concurrency.
Частые ошибки
- Забыть
[weak self]в замыкании, которое объект хранит (подписки, обработчики, таймеры). - Ставить
[weak self]везде подряд, даже вmap, и плодитьguard let self. [unowned self]в сетевом колбэке: экран закрыли, ответ пришёл, крэш.- Ожидать, что замыкание «запомнило» значение переменной без списка захвата.
Что спросят на собесе
- Как замыкание захватывает переменные, что такое список захвата?
- Что такое
@escapingи почему по умолчанию non-escaping? - Когда нужен
[weak self], а когда[unowned self]? - Что выведет код со счётчиком на замыкании?
- Что делает
@autoclosureи где он используется в стандартной библиотеке?