




In UIKit and MVVM-style iOS code, closures are commonly used for callbacks, event handlers, and async completion blocks. If ownership is not handled carefully, they can keep objects alive longer than intended and cause memory leaks.
Explain how retain cycles happen when a view controller or view model stores a closure that captures self. In your answer, cover:
[weak self] versus [unowned self].self inside the closure after using a capture list.The interviewer expects a practical conceptual explanation rather than a full ARC deep dive. You should describe the memory ownership issue, show a small Swift example, explain the trade-offs between weak and unowned captures, and mention common cases such as async callbacks, stored closures, and bindings.
A retain cycle happens when two objects keep strong references to each other, so ARC cannot deallocate either one. With closures, the closure itself is a reference type and can strongly capture self unless told otherwise.
class ViewModel {
var onUpdate: (() -> Void)?
}
class ViewController {
let viewModel = ViewModel()
func bind() {
viewModel.onUpdate = {
self.render()
}
}
func render() {}
}
A capture list lets you control how values are captured by a closure. Using [weak self] or [unowned self] changes the closure from strongly owning self to a non-owning reference.
viewModel.onUpdate = { [weak self] in
self?.render()
}
weak creates an optional reference that becomes nil when the object is deallocated, making it safer for asynchronous or longer-lived closures. unowned assumes the object will still exist when the closure runs, so it avoids optional handling but can crash if that assumption is wrong.
viewModel.onUpdate = { [unowned self] in
render()
}
Retain cycles are most common when a closure is stored as a property or retained by another object, such as a view model, timer, animation block, or network callback. Short-lived closures that are executed immediately usually do not create persistent leaks, but stored or delayed closures often do.
service.fetchData { [weak self] result in
guard let self = self else { return }
self.handle(result)
}