Retain Cycle is a situation in ARC where two or more objects reference each other through strong references, forming a closed loop. According to the Apple Memory Management Guide, 2026, a retain cycle prevents the deallocation of all objects in the cycle because each one has a retain count ≥ 1. Unlike a memory leak in GC, a retain cycle guarantees that objects remain alive as long as at least one external participant in the cycle is alive — and even after losing all external references, if the cycle is isolated.
Key Takeaways
Retain Cycle is a situation in which two or more objects own each other through strong references, creating a closed dependency graph. ARC cannot deallocate any of these objects because each one’s retain count is always ≥ 1: object A holds B, B holds A, and their counters never reach zero.
The problem occurs exclusively in reference counting systems (ARC, MRR). In Garbage Collection, the collector determines unreachability through the reference graph from the root set — cycles are not an obstacle. In ARC, however, a cycle is equivalent to a leak because deterministic deallocation by counting cannot resolve circular dependencies.
According to WWDC 2012 Session 406, retain cycle is the most common cause of memory leaks in Objective-C and Swift applications. Typical scenarios: parent-child relationships with delegates, closures capturing self, and layered architectures with bidirectional relationships.
Let’s examine classic retain cycle scenarios that every iOS developer encounters. Understanding these patterns is the foundation for writing safe code with ARC.
Classic scenario: a parent object (e.g., UIViewController) creates a child object and becomes its delegate. If both use strong references, a retain cycle occurs. The solution — the delegate should be weak.
// ERROR: retain cycle through strong delegate
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong via delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong by default
}
// FIX: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — does not hold
}
In the example, ParentVC holds a strong reference to ChildVC through its child property. ChildVC holds a strong reference to ParentVC through delegate. The cycle is closed. Fix: weak var delegate — the reference does not increase retain count, and ParentVC can be deallocated.
NSTimer is a classic source of retain cycles. The timer retains its target (usually self), and the target retains the timer through a property. Even if the timer is one-shot, it won’t be deallocated until invalidate is called. Solution: always call timer.invalidate() in deinit or viewDidDisappear.
In architectures with cascading ownership (coordinators, routers), multi-step cycles often occur: Coordinator → ViewController → ViewModel → Coordinator (via callback). Each strong reference in the chain must be carefully chosen — one weak reference at any link breaks the cycle.
Closures in Swift capture external variables by strong reference. If a closure is stored as a property of an object (e.g., a completion handler) and captures self, it creates a retain cycle: self → closure → self.
This is the most common source of retain cycles in modern Swift development. It occurs implicitly — a developer may not notice the capture of self in a closure, especially when using shorthand syntax without explicit self.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ Fix: capture list with weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
A capture list [weak self] creates a weak reference to self inside the closure. If DownloadService is deallocated before the closure executes, self becomes nil, and the code safely exits through guard. This is a standard pattern for asynchronous closures in Swift — it should be used whenever a closure is stored as a property.
unowned self is an alternative to weak self when self is guaranteed to outlive the closure. Example: synchronous closures that execute immediately (sorted, filter). In such cases self is definitely alive, and unowned is safe. However, unowned crashes when accessing a deallocated object — therefore weak is considered the safe default.
Detecting retain cycles early is critically important for application performance. Let’s review the main tools and techniques for identifying cyclic references in iOS development.
Xcode Memory Debugger (Debug Memory Graph) is a visual tool that shows the graph of objects in memory with their references. A retain cycle appears as a closed chain of strong arrows. To launch: click the Debug Memory Graph button in the Debug area panel while the app is running. Each object is shown with its type, address, and list of references.
Instruments Leaks is a profiler for automatic leak detection. It records allocations and analyzes the reference graph in real time. It detects not only retain cycles but also forgotten references, unallocated ViewControllers, and other leaks. Leaks points to the exact object and the holding chain.
The simplest method is to add a print statement in the deinit of each key class. If deinit is not called when the object is expected to be destroyed, there is a retain cycle. This method requires no tools and is effective for initial diagnosis.
| Tool | Type | When to use |
|---|---|---|
| Memory Debugger | Visual graph | Manual check after navigation |
| Instruments Leaks | Automated analysis | Regression testing, CI |
| deinit print | Manual logging | Development, code review |
| Malloc Scribble | Runtime flag | Debugging use-after-free |
Recommended approach: use deinit logging during development, Memory Debugger during manual testing, and Instruments Leaks in the CI/CD pipeline for automated regression leak detection.
Preventing retain cycles is easier than fixing them in production. Here are a few rules that minimize the risk of cyclic references.
All delegates and dataSources should be weak. This rule is built into UIKit: all delegate protocols in the Apple SDK are declared with weak properties (UITableView.delegate, UICollectionView.dataSource). For your own protocols, use weak var delegate: MyDelegate? and inherit the protocol from AnyObject.
Any closure that is stored as a property (completion handler, callback) and captures self must use [weak self] in the capture list. The exception is closures that execute immediately and are not stored (sorted, map, filter). For those, unowned self is safe.
In complex architectures (VIPER, Coordinators, Redux), track the direction of strong references. The owner holds a strong reference to the subordinate, but the subordinate must reference the owner only through weak or unowned. Unidirectional data flow simplifies reference management.
// Example: check with deinit logging
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// Usage: all ViewControllers inherit BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// When closing ProfileVC expect "✅ ProfileVC deallocated" in console
A base class with deinit logging provides instant feedback. If the message does not appear when the screen is expected to close, there is a retain cycle in this class. Add this practice to the project template for all ViewControllers.
Frequently Asked Questions
Retain cycle is a specific ARC problem where a closed loop of strong references blocks deallocation. In GC, the collector analyzes reachability from the root set, not reference counts — therefore cycles are not leaks. In ARC, however, any isolated cycle is a guaranteed leak.
A weak reference does not increase the retain count of an object. If you replace one of the strong references in a cycle with weak, each object’s retain count can reach zero. After the object is deallocated, the weak reference is automatically set to nil, preventing access to deallocated memory.
Yes, a retain cycle can include any number of objects: A → B → C → A. To break it, you only need to break one link in the cycle — replace any strong reference with weak or unowned. Tools show the entire graph, not just pairs of objects.
GCD (Grand Central Dispatch) does not store the closure after execution. The DispatchWorkItem executes and is released, even if the closure captures self. A retain cycle only occurs when a closure is stored as a property (completion handler in a class), not when passed to a queue.
Instruments Leaks does not always find temporary retain cycles (lasting seconds) or cyclic references in C/C++ objects through bridging. For a thorough check, use Memory Debugger manually along with deinit logging of all key objects in the scene.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also