Retain Cycle — essence, causes and elimination in app development

Author: IT Sectr Published: 2026-03-29 Reading time: 8 min

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 — a closed chain of strong references where objects cannot be released by ARC
  • Cause — two (or more) objects hold strong references to each other, making retain count zeroing impossible
  • Consequence — memory leak: objects remain in memory forever, RAM consumption grows
  • Solution — replace one of the strong references in the cycle with weak or unowned
  • Diagnosis — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

What is Retain Cycle?

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.

Retain cycle examples in iOS development

Let’s examine classic retain cycle scenarios that every iOS developer encounters. Understanding these patterns is the foundation for writing safe code with ARC.

Parent-Child with delegate

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.

swift
// 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 and retain cycle

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.

Layered architectures

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.

Retain Cycle in Swift Closures

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.

swift
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 in closures

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.

How to detect retain cycle: diagnostic tools

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

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

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.

Deinit logging

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.

ToolTypeWhen to use
Memory DebuggerVisual graphManual check after navigation
Instruments LeaksAutomated analysisRegression testing, CI
deinit printManual loggingDevelopment, code review
Malloc ScribbleRuntime flagDebugging 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.

Retain cycle prevention and best practices

Preventing retain cycles is easier than fixing them in production. Here are a few rules that minimize the risk of cyclic references.

Weak delegate rule

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.

Capture list in closures

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.

Architecture review

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.

swift
// 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

How is retain cycle different from a memory leak in GC?

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.

How does a weak reference break a retain cycle?

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.

Can a retain cycle consist of three or more objects?

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.

Why doesn’t GCD DispatchWorkItem create a retain cycle?

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.

What types of retain cycles are not detected by Instruments?

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

  • Retain Cycle — a closed chain of strong references that blocks object deallocation in ARC
  • Causes — delegates with strong references, closures capturing self, bidirectional parent-child relationships
  • Solution — replacing one strong reference with weak or unowned breaks the cycle
  • Closures — stored completion handlers must always use [weak self]
  • Delegates — always weak; the delegate protocol must inherit from AnyObject
  • Detection — Xcode Memory Debugger, Instruments Leaks, deinit logging
  • Prevention — unidirectional data flow, weak delegates, capture lists, base deinit class

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.

Discuss the project

Read also