Answer
The core idea
A retain cycle happens when two or more reference types hold strong references to each other, so ARC can never reduce their reference counts to zero. The objects are no longer reachable in a useful way, but they still stay alive because each object is keeping the other alive.
The short interview answer is: ARC frees objects when the strong reference count reaches zero. A retain cycle prevents that from ever happening.
1. Simple object-to-object cycle
This often shows up in app architecture when a view controller owns a presenter, and the presenter also strongly owns the view controller. The example below is intentionally wrong: both sides are strong, so it creates a real retain cycle.
import Foundation
final class InterviewViewController {
var presenter: InterviewPresenter?
deinit {
print("InterviewViewController deinit")
}
}
final class InterviewPresenter {
var view: InterviewViewController? // ❌ Strong capture of view controller
deinit {
print("InterviewPresenter deinit")
}
}
var viewController: InterviewViewController? = InterviewViewController()
var presenter: InterviewPresenter? = InterviewPresenter()
viewController?.presenter = presenter
presenter?.view = viewController
viewController = nil
presenter = nilAfter viewController = nil and presenter = nil, you might expect both objects to deallocate. They do not, because InterviewViewController still strongly owns presenter, and InterviewPresenter still strongly owns viewController. That strong-reference loop keeps both objects alive, so the deinit prints never happen.
2. Closure capture cycle
Closures are a very common interview example because the cycle is less obvious in code review. The object owns the closure property, and the closure captures the object.
final class ProfileViewController: UIViewController {
var onSave: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onSave = {
self.dismiss(animated: true) // ❌ Strong capture of self
}
}
deinit {
print("ProfileViewController deinit")
}
}That creates this ownership graph:
ProfileViewControllerstrongly ownsonSaveonSavestrongly capturesself(ProfileViewController)
So the view controller never goes away unless the closure is cleared or the capture changes.
3. How to break the cycle
You break a retain cycle by deciding which reference should not participate in ownership.
Use weak when the reference can become nil
weak is the safest default for delegates, parent links, and closure captures where the referenced object may disappear before the closure runs.
final class ProfileViewController: UIViewController {
var onSave: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onSave = { [weak self] in
self?.dismiss(animated: true) // ✅ Safe
}
}
}Use unowned only when lifetime is guaranteed
unowned avoids optionals, but it is only correct when you know the referenced object will still exist when the reference is used. If that assumption is wrong, you get a crash.
Good interview wording is: use weak by default; use unowned only when the ownership relationship guarantees the other object outlives this reference.
Make delegates weak
Making delegates weak prevents retain cycles and memory leaks, ensuring the delegator does not strongly hold the delegate.
protocol ChildCoordinatorDelegate: AnyObject {
func childDidFinish()
}
final class ChildCoordinator {
weak var delegate: ChildCoordinatorDelegate?
}4. Why it matters
Retain cycles are not just a theoretical memory-management detail. If leaked objects stay alive, they can keep doing work, holding memory, and reacting to events long after that part of the app should be gone.
Common consequences in a real app:
- dismissed screens stay in memory, so memory usage keeps growing as the user navigates
- timers, observers, or subscriptions keep firing after a screen disappears
- network requests or background work continue longer than expected
- old view controllers or presenters react to new events and create confusing bugs
- in severe cases, the app gets slower, uses more battery, or is terminated for high memory usage
5. How to debug it
Keep debugging simple:
- Add a
deinitprint to the object that should go away. If the screen is dismissed anddeinitnever runs, something is still retaining it. - Open Xcode's Memory Graph Debugger and inspect the object that should have been released. Follow the strong reference chain until you find the cycle, which is often a delegate, stored closure, timer, or presenter/view reference.
Interview angle
Lead with ARC: a retain cycle is a strong-reference loop between reference types. Then name the common sources: object graphs, delegates, and stored closures capturing self. Finish with the practical fix: make the non-owning edge weak when lifetime is uncertain, use unowned only when lifetime is guaranteed, and verify the change with deinit and Memory Graph.