Swiftyn LogoSwiftyn
LearnInterview PrepRoadmapsArchitect Profile
Swift LanguageSwiftUIUIKitiOS ConceptsmacOS

Swift Language Topics

Introduction to SwiftVariables and ConstantsData TypesType InferenceOperatorsStrings and CharactersBooleansTuplesIf Else StatementsSwitch StatementsGuard StatementsLoopsBreak and ContinueArraysDictionariesSetsCollection OperationsFunctionsFunction ParametersReturn TypesInout ParametersVariadic ParametersClosuresTrailing ClosuresEscaping ClosuresAuto ClosuresCapture ListsOptionalsOptional BindingNil CoalescingOptional ChainingImplicitly Unwrapped OptionalsStructuresClassesPropertiesComputed PropertiesProperty ObserversMethodsInitializationDeinitializationInheritancePolymorphismEncapsulationAccess ControlStatic vs Class MethodsProtocolsProtocol ExtensionsProtocol CompositionAssociated TypesExtensionsGenericsGeneric ConstraintsOpaque TypesExistential TypesType CastingAny and AnyObjectNested TypesSubscriptsKeyPathsThrowing FunctionsDo Try CatchCustom ErrorsResult TypeARCStrong ReferencesWeak ReferencesUnowned ReferencesRetain CyclesMemory LeaksCopy on Write
Browse Swift Language Topics
Introduction to SwiftVariables and ConstantsData TypesType InferenceOperatorsStrings and CharactersBooleansTuplesIf Else StatementsSwitch StatementsGuard StatementsLoopsBreak and ContinueArraysDictionariesSetsCollection OperationsFunctionsFunction ParametersReturn TypesInout ParametersVariadic ParametersClosuresTrailing ClosuresEscaping ClosuresAuto ClosuresCapture ListsOptionalsOptional BindingNil CoalescingOptional ChainingImplicitly Unwrapped OptionalsStructuresClassesPropertiesComputed PropertiesProperty ObserversMethodsInitializationDeinitializationInheritancePolymorphismEncapsulationAccess ControlStatic vs Class MethodsProtocolsProtocol ExtensionsProtocol CompositionAssociated TypesExtensionsGenericsGeneric ConstraintsOpaque TypesExistential TypesType CastingAny and AnyObjectNested TypesSubscriptsKeyPathsThrowing FunctionsDo Try CatchCustom ErrorsResult TypeARCStrong ReferencesWeak ReferencesUnowned ReferencesRetain CyclesMemory LeaksCopy on Write
Swiftyn Logo

Swiftyn

The go-to platform for Apple developers. Swift, SwiftUI, and beyond.

Questions? Email us at support@swe180.com

Categories

  • SwiftUI
  • Swift Language
  • Xcode
  • visionOS

Our Products

  • SWE180
  • One Percent Engineer

Resources

  • About
  • RSS Feed
  • Apple Developer

© 2026 Swiftyn. All rights reserved.

Privacy PolicyTerms of Service

Swiftyn is the premier learning platform and developer resource for mastering the Apple ecosystem. Whether you are an aspiring iOS developer looking to learn Swift 6, a macOS engineer diving into advanced system architecture, or an XR pioneer building the future with visionOS, our beautifully crafted tutorials, roadmaps, and interview prep guides have you covered. Built by Apple developers, for Apple developers.

Swift Language10 min read

Mastering Retain Cycles in Swift: Preventing Memory Leaks

Retain cycles are a common source of memory leaks in Swift applications, preventing objects from being deallocated and consuming system resources. Understanding how Automatic Reference Counting (ARC) works and how to break these cycles using weak and unowned references is crucial for building robust and performant apps.

Understanding ARC and Memory Management in Swift

Swift uses Automatic Reference Counting (ARC) to manage your app's memory. When an instance of a class is created, ARC allocates a chunk of memory to store information about that instance. As long as at least one strong reference to that instance exists, ARC keeps it in memory. When the last strong reference is removed, ARC deallocates the instance and frees up its memory. This automatic process simplifies memory management significantly compared to manual approaches.

How ARC Works:

  1. Allocation: Memory is allocated for a new class instance.
  2. Tracking References: ARC keeps a count of how many strong references point to an instance.
  3. Deallocation: When the strong reference count drops to zero, the instance's deinit method is called, and its memory is reclaimed.

Understanding ARC is the foundation for comprehending retain cycles.

What is a Retain Cycle?

A retain cycle occurs when two or more objects hold strong references to each other, forming a closed loop. Because each object in the cycle maintains a strong reference to the other, their reference counts never drop to zero, even if they are no longer needed by the rest of the application. As a result, ARC cannot deallocate these objects, leading to a memory leak – the memory they occupy is never released until the app terminates.

Imagine two objects, Person and Apartment. If a Person strongly refers to an Apartment and that Apartment strongly refers back to the Person, neither can be deallocated as long as the other exists, even if there are no external strong references to either of them.

swift
import Foundation

class Person {
    let name: String
    var apartment: Apartment?

    init(name: String) {
        self.name = name
        print("Person \(name) is being initialized")
    }

    deinit {
        print("Person \(name) is being deinitialized")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?

    init(unit: String) {
        self.unit = unit
        print("Apartment \(unit) is being initialized")
    }

    deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}

var john: Person? // External strong reference 1
var unit4A: Apartment? // External strong reference 2

print("--- Creating instances ---")
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

print("--- Creating strong references to each other ---")
john!.apartment = unit4A
unit4A!.tenant = john

print("--- Releasing external strong references ---")
john = nil
unit4A = nil

// Expected Output (without retain cycle):
// Person John Appleseed is being initialized
// Apartment 4A is being initialized
// Person John Appleseed is being deinitialized
// Apartment 4A is being deinitialized

// Actual Output (with retain cycle):
// Person John Appleseed is being initialized
// Apartment 4A is being initialized
// Person John Appleseed is being deinitialized (never called)
// Apartment 4A is being deinitialized (never called)
// No deinit messages, indicating a memory leak.

Breaking Retain Cycles with Weak and Unowned References

Swift provides two primary ways to resolve retain cycles: weak references and unowned references. Both allow you to refer to another instance without creating a strong reference, thus preventing ARC from keeping the instance alive.

Weak References (weak var)

A weak reference is a reference that doesn't keep a strong hold on the instance it refers to. It's automatically set to nil when the instance it refers to is deallocated. Because of this behavior, weak references are always declared as optional types (SomeType?). They are particularly useful when the referenced object might be deallocated independently of the referencing object, or when two objects have a peer-to-peer relationship where either can exist without the other.

Use weak when:

  • The referenced object has a shorter or independent lifetime.
  • The reference can legitimately become nil at some point.
  • You're dealing with delegate patterns (e.g., UITableViewDelegate, UIViewControllerDelegate).

Unowned References (unowned var)

An unowned reference is used when the other instance has the same lifetime or a longer lifetime than the referencing instance. Unlike weak references, an unowned reference is always expected to have a value. Therefore, it's not declared as an optional type, and you don't need to unwrap it. If you try to access an unowned reference after the instance it refers to has been deallocated, your app will crash at runtime.

Use unowned when:

  • The referenced object is guaranteed to be alive for the entire lifetime of the referencing object.
  • The reference should never become nil once it's set.
  • You're dealing with parent-child relationships where the child always has a parent, or closures that will only be called while self is guaranteed to be alive.

Let's revisit our Person and Apartment example and fix the retain cycle.

swift
import Foundation

class PersonFixed {
    let name: String
    var apartment: ApartmentFixed?

    init(name: String) {
        self.name = name
        print("PersonFixed \(name) is being initialized")
    }

    deinit {
        print("PersonFixed \(name) is being deinitialized")
    }
}

class ApartmentFixed {
    let unit: String
    // Using 'weak' because an Apartment might exist without a tenant, 
    // or a tenant might leave, making this reference nil.
    weak var tenant: PersonFixed?

    init(unit: String) {
        self.unit = unit
        print("ApartmentFixed \(unit) is being initialized")
    }

    deinit {
        print("ApartmentFixed \(unit) is being deinitialized")
    }
}

var johnFixed: PersonFixed?
var unit4AFixed: ApartmentFixed?

print("\n--- Creating instances (Fixed) ---")
johnFixed = PersonFixed(name: "John Appleseed")
unit4AFixed = ApartmentFixed(unit: "4A")

print("--- Creating strong/weak references (Fixed) ---")
johnFixed!.apartment = unit4AFixed
unit4AFixed!.tenant = johnFixed

print("--- Releasing external strong references (Fixed) ---")
johnFixed = nil
unit4AFixed = nil

// Expected Output:
// PersonFixed John Appleseed is being initialized
// ApartmentFixed 4A is being initialized
// PersonFixed John Appleseed is being deinitialized
// ApartmentFixed 4A is being deinitialized
// Deinit messages confirm successful deallocation.

Retain Cycles with Closures

Closures in Swift are reference types, meaning they capture and store references to any constants and variables from the context in which they are defined. If a closure captures self strongly, and self also holds a strong reference to that closure, a retain cycle occurs.

This is a very common source of memory leaks, especially in asynchronous operations, delegates, and property observers. You break closure-based retain cycles by using a capture list.

A capture list defines the rules for how specific variables are captured within the closure body:

  • [weak self] : Captures self as a weak optional. If self is deallocated, self inside the closure becomes nil.
  • [unowned self] : Captures self as an unowned non-optional. You must guarantee that self will always be alive when the closure is executed. If self is deallocated before the closure is called, your app will crash.

Always consider the lifecycle of self relative to the closure's execution: if the closure might outlive self, use weak. If the closure is guaranteed to only execute while self is alive, use unowned for a small performance benefit and cleaner code (no optional unwrapping).

swift
import Foundation

class DataFetcher {
    var data: String = "Initial Data"
    var completionHandler: (() -> Void)?

    init() {
        print("DataFetcher initialized")
    }

    func fetchDataWithLeak() {
        // Strong capture of self in the closure
        self.completionHandler = { 
            self.data = "Fetched Data with Leak"
            print("Data updated: \(self.data)")
        }
    }

    func fetchDataWithoutLeakWeak() {
        // Weak capture of self in the closure
        self.completionHandler = { [weak self] in
            // Must safely unwrap self as it could be nil
            guard let self = self else { 
                print("DataFetcher deallocated before completionHandler was called (weak).")
                return 
            }
            self.data = "Fetched Data (Weak Reference)"
            print("Data updated: \(self.data)")
        }
    }

    func fetchDataWithoutLeakUnowned() {
        // Unowned capture of self in the closure
        // Use only if self is guaranteed to be alive when closure executes
        self.completionHandler = { [unowned self] in
            self.data = "Fetched Data (Unowned Reference)"
            print("Data updated: \(self.data)")
        }
    }

    deinit {
        print("DataFetcher deinitialized")
    }
}

// --- Leaky Scenario ---
print("\n--- Leaky Scenario ---")
var fetcher1: DataFetcher? = DataFetcher()
fetcher1?.fetchDataWithLeak()
// At this point, fetcher1 holds a strong ref to completionHandler, 
// and completionHandler closure strongly captures fetcher1.
fetcher1 = nil // DataFetcher will NOT be deinitialized here due to retain cycle

// --- Weak Fix Scenario ---
print("\n--- Weak Fix Scenario ---")
var fetcher2: DataFetcher? = DataFetcher()
fetcher2?.fetchDataWithoutLeakWeak()
// Now, completionHandler weakly captures fetcher2.
fetcher2 = nil // DataFetcher WILL be deinitialized here.

// --- Unowned Fix Scenario (Careful usage!) ---
// This would typically be used for closures that are immediately executed
// or where their lifetime is strictly bounded by self's lifetime.
print("\n--- Unowned Fix Scenario ---")
class Owner {
    var worker: Worker // Strong ref to Worker
    init() {
        print("Owner initialized")
        self.worker = Worker(callback: { [unowned self] in
            print("Owner received callback from worker. self is: \(self)") // self is guaranteed to exist here
        })
    }
    deinit { print("Owner deinitialized") }
}

class Worker {
    var action: () -> Void
    init(callback: @escaping () -> Void) {
        print("Worker initialized")
        self.action = callback
    }
    func doWork() { action() }
    deinit { print("Worker deinitialized") }
}

var myOwner: Owner? = Owner()
myOwner?.worker.doWork()
myOwner = nil // Both Owner and Worker should deinitialize successfully.

Common Scenarios for Retain Cycles

Retain cycles aren't limited to simple class properties. They can manifest in several common patterns:

  1. Delegate Patterns: When a delegate protocol is implemented by a class, and the delegating object holds a strong reference to its delegate, while the delegate also holds a strong reference back to the delegating object (often self). The fix is to declare the delegate property as weak.

    • Compatibility: Applies to all iOS/macOS versions.
  2. Closures in View Controllers: As seen above, closures that refer to self (e.g., network callbacks, animation blocks, timer handlers) can create cycles if the view controller also owns the closure (or a wrapper around it).

    • Compatibility: Applies to all iOS/macOS versions.
  3. Observers and Notifications: If an object adds itself as an observer to NotificationCenter and the observation block captures self strongly, but the observer isn't explicitly removed or the NotificationCenter's token isn't properly managed, a cycle can occur.

    • Compatibility: Applies to all iOS/macOS versions. Modern NotificationCenter APIs with addObserver(forName:object:queue:using:) often return a NotificationToken that you should hold onto and manage its lifecycle, or use addObserver(self, selector...) and ensure removeObserver is called.
  4. Target-Action Patterns: While less common for direct strong cycles, if a target for a control action captures self in an unusual way or a custom UIControl subclass creates a strong reference that isn't broken, it's possible. Standard addTarget(_:action:for:) is weak by default on the target.

    • Compatibility: Applies to all iOS/macOS versions.
  5. Child-Parent Relationships: If a parent object owns a child, and the child needs to refer back to its parent. If this back-reference is strong, a cycle forms. Generally, the child's reference to the parent should be weak if the parent can exist without the child, or unowned if the child strictly cannot exist without the parent. For example, a ViewController strongly owns its ViewModel, and the ViewModel has a weak reference back to its ViewController (or a protocol implemented by it).

swift
import UIKit

// 1. Delegate Pattern Example (common in UIKit)
protocol MyDelegate: AnyObject { // 'AnyObject' constraint allows 'weak' references
    func didPerformAction()
}

class ResponsibleClass {
    weak var delegate: MyDelegate?

    func performAction() {
        print("ResponsibleClass performs action")
        delegate?.didPerformAction()
    }
    deinit { print("ResponsibleClass deinitialized") }
}

class DelegatingViewController: UIViewController, MyDelegate {
    var responsible: ResponsibleClass?

    override func viewDidLoad() {
        super.viewDidLoad()
        responsible = ResponsibleClass()
        responsible?.delegate = self // Weak reference from ResponsibleClass to self
    }

    func didPerformAction() {
        print("DelegatingViewController received action callback")
    }
    deinit { print("DelegatingViewController deinitialized") }
}

// Simulation for demonstration (normally managed by UINavigationController for VCs):
// var vc: DelegatingViewController? = DelegatingViewController()
// vc?.viewDidLoad() // Simulate viewDidLoad
// vc?.responsible?.performAction()
// vc = nil // Both VC and ResponsibleClass should deallocate if weak is used correctly.


// 3. NotificationCenter Observer Example (modern approach)
class NotifyingWorker {
    let notificationName = Notification.Name("WorkerDidFinish")
    func startWork() {
        print("Worker started")
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            NotificationCenter.default.post(name: self.notificationName, object: nil)
            print("Worker finished and posted notification")
        }
    }
    deinit { print("NotifyingWorker deinitialized") }
}

class NotificationObserver: NSObject {
    var worker: NotifyingWorker?
    var observation: NSObjectProtocol?

    override init() {
        super.init()
        worker = NotifyingWorker()
        // Modern way to observe: the returned token handles memory management
        observation = NotificationCenter.default.addObserver(forName: worker!.notificationName, object: nil, queue: .main) { [weak self] notification in
            guard let self = self else { return }
            print("NotificationObserver received notification: \(notification.name.rawValue) - from \(self)")
        }
    }

    deinit {
        print("NotificationObserver deinitialized")
        // The observation token is automatically handled if held in a strong reference.
        // If you were using the old selector-based addObserver, you'd need removeObserver.
    }
}

// var observer: NotificationObserver? = NotificationObserver()
// observer?.worker?.startWork()
// observer = nil // Should deallocate both observer and worker after a short delay.

Identifying and Debugging Retain Cycles

Identifying retain cycles can sometimes be tricky. Xcode provides powerful tools to help:

  • Memory Graph Debugger: This is your primary tool. During a debugging session, activate the Memory Graph Debugger (Debug > Debug Workflow > View Memory Graph Hierarchy or click the '...' button in the Debug Navigator pane and select 'Show Memory Graph'). It visually represents your app's object graph, showing strong references. Look for cycles where objects point to each other without an obvious external release path. If an object you expect to be deallocated still appears in the graph, it's likely part of a retain cycle.

  • deinit methods: Implement deinit methods in your classes and add print statements. If an object isn't deinitialized when you expect it to be (e.g., after dismissing a view controller or setting a strong reference to nil), it's a strong indicator of a memory leak.

  • Instruments (Leaks, Allocations): The Instruments tool, particularly the 'Leaks' template, can detect memory leaks by monitoring memory usage over time. The 'Allocations' instrument can also help you track object lifecycles and reference counts, showing you which objects persist longer than they should.

  • Code Review: Proactively review your code for common retain cycle patterns, especially involving closures, delegates, and parent-child relationships.

Automatic Memory Management is Foolproof

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Automatic Memory Management is Foolproof

Many believe Swift's ARC completely eliminates memory management concerns. While it handles *most* cases, explicit attention is required to prevent retain cycles, where objects strongly reference each other, preventing deallocation.

swift
class A { var b: B?; deinit { print("A deinit") } }
class B { var a: A?; deinit { print("B deinit") } }
var a: A? = A(); var b: B? = B()
a?.b = b; b?.a = a
a = nil; b = nil // Leaked!

WHAT HAPPENS INTERNALLY? (ARC & Reference Counts)

ARC maintains two counts for each class instance: a strong reference count and a weak reference count. When a strong reference is made, the strong count increments. When it's removed, the strong count decrements. Only when the strong count reaches zero is the instance deallocated. Weak references do not affect the strong count and are automatically nilled out when the instance is deallocated.

Memory Instance
Strong Reference Counter
Weak Reference Counter
Deallocation Logic
1

1. Instance Creation

Memory allocated, strong count = 1.

2

2. Strong Reference Added

Strong count increments.

3

3. Weak/Unowned Reference Added

Strong count unchanged, weak count (if applicable) increments.

4

4. Strong Reference Removed

Strong count decrements.

5

5. Strong Count to Zero

Instance deallocated, `deinit` called, weak references set to nil.

6

6. Retain Cycle

Strong count never reaches zero due to circular holding.

Visualized execution hierarchy.

Powerful Guarantees

ARC's Strong Guarantee

An object will be deallocated *only* when its strong reference count drops to zero.

Weak Reference Safety

A `weak` reference will automatically become `nil` when its referenced object is deallocated, preventing crashes from dangling pointers.

Unowned Reference Expectation

An `unowned` reference assumes the referenced object will always be alive when accessed; accessing a deallocated unowned reference will crash.

REAL PRODUCTION EXAMPLE: Network Manager Closure Leak

A `ViewController` creates a `NetworkManager` instance. The `NetworkManager` has a `fetchData` method that takes a completion handler closure. If this closure captures `self` (the `ViewController`) strongly without a capture list, and the `NetworkManager` is owned by the `ViewController`, a retain cycle forms. When the `ViewController` is dismissed, neither it nor the `NetworkManager` (and its closure) can be deallocated.

Impact / Results
Increased memory usage over time.
App slowdown, especially with repeated use of leaked VCs.
Unpredictable behavior if leaked objects continue to execute tasks.
THE FIX or SOLUTION: Capture List for Closures
swift
class ViewController: UIViewController {
    let networkManager = NetworkManager() // Strong reference from VC to manager

    func viewDidLoad() {
        super.viewDidLoad()
        networkManager.fetchData { [weak self] data, error in
            guard let self = self else { return } // Safely unwrap weak self
            // ... handle data update on UI ...
        }
    }
    deinit { print("ViewController deinitialized") }
}

class NetworkManager {
    // ... other network logic ...
    func fetchData(completion: @escaping (String?, Error?) -> Void) {
        // Simulate async operation
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completion("Some Data", nil)
        }
    }
    deinit { print("NetworkManager deinitialized") }
}

INTERVIEW PERSPECTIVE

Common Question

“Explain a common retain cycle scenario and how to resolve it.”

Strong Answer

A classic scenario is a `ViewController` strongly owning a `ViewModel` (or some data provider), and that `ViewModel` having a closure property (e.g., a callback) that itself captures the `ViewController` (`self`) strongly. This creates a cycle: `ViewController` -> `ViewModel` -> `Closure` -> `ViewController`. To resolve this, use a capture list `[weak self]` or `[unowned self]` within the closure when referencing the `ViewController`. This breaks the strong reference `Closure` -> `ViewController`, allowing both to be deallocated.

Interviewers Expect you to understand:
  • Clear explanation of the cycle.
  • Correct application of `weak` or `unowned`.
  • Understanding when to pick `weak` vs. `unowned` (optionality, lifetime guarantees).
  • Mentioning `deinit` print statements and Memory Graph Debugger for identification.
KEY TAKEAWAY

Always look for explicit strong references between objects that need to refer to each other, especially with closures and delegate patterns. When in doubt about lifetimes, `weak` is safer due to its optional nature; use `unowned` only when you're 100% certain the referenced object will outlive or have the same lifespan as the referencing object.

Common Interview Questions

What's the difference between `weak` and `unowned` references?

`weak` references are always optional (`Type?`) and automatically become `nil` when the referenced object is deallocated. They are suitable when the referenced object has a shorter or independent lifetime. `unowned` references are non-optional (`Type`) and assume the referenced object will always be alive when accessed. Accessing an unowned reference after its object has been deallocated will cause a runtime crash. Use `unowned` when the lifetimes are strictly dependent and the referenced object will live at least as long as the referencing object.

Why do retain cycles only happen with classes and not structs?

Retain cycles only occur with classes because classes are reference types. Their instances are stored on the heap, and ARC tracks their strong references. Structs, on the other hand, are value types. They are copied when passed around, and their memory management is handled automatically by the stack or by the containing reference type, without ARC tracking individual references to them.

Can I have a retain cycle with a `protocol`?

Yes, if the protocol specifies a class-only requirement using `AnyObject` or inherits from `class` (e.g., `protocol MyDelegate: AnyObject`). If a `delegate` property adhering to such a protocol is declared strong and the delegate also strongly holds the delegating object, a retain cycle can form. This is why delegates are almost always declared as `weak var delegate: MyDelegate?` and the protocol itself often conforms to `AnyObject`.

How can I check for retain cycles in my app?

Xcode's Memory Graph Debugger is the most effective way. Run your app in the debugger, then click the 'Debug Memory Graph' button (or go to Debug > Debug Workflow > View Memory Graph Hierarchy). This tool visually shows object relationships and strong references, making it easier to spot circular dependencies. Additionally, adding `print("\(self) deinitialized")` to your classes' `deinit` methods can help identify objects that are not being deallocated as expected.

When should I use `unowned(safe)` vs `unowned(unsafe)`?

The terms `unowned(safe)` and `unowned(unsafe)` are primarily internal compiler details or related to Objective-C bridge features. In modern Swift, the default `unowned` keyword works as `unowned(safe)`. There's rarely a good reason to explicitly use `unowned(unsafe)`, as it bypasses runtime safety checks, leading to potential memory corruption without immediately crashing. Always prefer `weak` or `unowned` without the `(unsafe)` specifier unless you have a deep understanding of memory management and specific performance-critical scenarios that justify bypassing safety.

#Swift#Memory Management#ARC#Retain Cycles#Weak References#Unowned References