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 Language12 min read

Mastering Weak References in Swift: Preventing Retain Cycles

Weak references are a critical concept in Swift memory management, especially when dealing with classes and closures. Mastering when and how to use them is key to preventing memory leaks and building performant, reliable applications. This article dives deep into weak references, strong reference cycles, and practical application.

Understanding Automatic Reference Counting (ARC)

Before diving into weak references, it's essential to have a solid understanding of how Swift manages memory. Swift uses Automatic Reference Counting (ARC) to track and manage your app's memory usage. ARC automatically deallocates instances of classes when they are no longer needed, freeing up memory. It does this by counting the number of strong references to an instance. An instance is kept in memory as long as at least one strong reference to it exists.

Every time you create a new strong reference to an instance, its reference count increases. When a strong reference is removed (e.g., a variable goes out of scope, or nil is assigned to it), the reference count decreases. Once the reference count drops to zero, ARC deallocates the instance.

While ARC handles most memory management complexities for you, it's not perfect. It can't resolve strong reference cycles on its own, which is where weak and unowned references come into play.

The Problem: Strong Reference Cycles (Memory Leaks)

A strong reference cycle occurs when two or more class instances hold strong references to each other, preventing them from being deallocated even when they are no longer accessible from the rest of your application. Both instances' reference counts never drop to zero, leading to a memory leak. This is a common pitfall in object-oriented programming.

Consider a Person class and a House class. A person might own a house, and a house might have an owner. If both Person and House have strong references to each other, neither will ever be deallocated, even if you try to nil out your main references to them.

Let's illustrate this with a code example:

swift
import Foundation

class Person {
    let name: String
    var house: House?

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

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

class House {
    let address: String
    var owner: Person?

    init(address: String) {
        self.address = address
        print("House at \(address) is initialized")
    }

    deinit {
        print("House at \(address) is being deinitialized")
    }
}

var john: Person? = Person(name: "John Appleseed")
var apartment: House? = House(address: "123 Swift Lane")

// Creating a strong reference cycle
john!.house = apartment
apartment!.owner = john

print("Setting john and apartment to nil...")
john = nil
apartment = nil
print("Finished setting john and apartment to nil.")

// Expected Output (but doesn't happen due to cycle):
// Person John Appleseed is initialized
// House at 123 Swift Lane is initialized
// Setting john and apartment to nil...
// Person John Appleseed is being deinitialized
// House at 123 Swift Lane is being deinitialized

// Actual Output (no deinitialization calls):
// Person John Appleseed is initialized
// House at 123 Swift Lane is initialized
// Setting john and apartment to nil...
// Finished setting john and apartment to nil.

Solving Strong Reference Cycles with Weak References

To break a strong reference cycle, you need to change one of the strong references into either a weak or unowned reference. A weak reference does not create a strong hold on the instance it refers to, so it doesn't prevent ARC from deallocating that instance. Since a weak reference doesn't hold a strong reference, the instance it refers to might be deallocated at any point. Because of this, a weak reference must always be a variable (never a constant) and must be of an optional type. ARC automatically sets a weak reference to nil when the instance it refers to is deallocated.

In our Person and House example, it makes sense for a House to have a strong reference to its owner, but the owner's house reference can be weak. The owner might come and go, but the house still stands. Or, conversely, a Person might exist without a House, and a House without an Owner.

Let's apply the weak keyword to our example:

swift
import Foundation

class Person {
    let name: String
    var house: House?

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

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

class House {
    let address: String
    // Use 'weak var' to break the strong reference cycle
    weak var owner: Person?

    init(address: String) {
        self.address = address
        print("House at \(address) is initialized")
    }

    deinit {
        print("House at \(address) is being deinitialized")
    }
}

var john: Person? = Person(name: "John Appleseed")
var apartment: House? = House(address: "123 Swift Lane")

john!.house = apartment
apartment!.owner = john // This is now a weak reference

print("Setting john and apartment to nil...")
john = nil
apartment = nil
print("Finished setting john and apartment to nil.")

// Output:
// Person John Appleseed is initialized
// House at 123 Swift Lane is initialized
// Setting john and apartment to nil...
// Person John Appleseed is being deinitialized
// House at 123 Swift Lane is being deinitialized
// Finished setting john and apartment to nil.

Weak References in Closures (Capture Lists)

Strong reference cycles aren't limited to two class instances directly referencing each other. They commonly occur when a closure captures self (or another class instance) strongly, and that class instance also holds a strong reference to the closure. This creates a circular dependency.

Consider a ViewController that has a completion handler closure. If the closure captures self strongly, and the ViewController itself strongly owns that closure, you have a retain cycle.

To prevent this, you use capture lists within the closure definition. A capture list specifies how an instance (like self) should be captured by the closure – either weak or unowned.

Here's an example of a potential retain cycle with a closure and how to fix it with a weak capture:

swift
import Foundation

class APIClient {
    static let shared = APIClient()
    func fetchData(completion: @escaping (String) -> Void) {
        // Simulate network request
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            completion("Data received!")
        }
    }
}

class DataDownloader {
    var data: String? // iOS 13+ / macOS 10.15+

    deinit {
        print("DataDownloader deinitialized")
    }

    func startDownload() {
        // Potential retain cycle: self (DataDownloader) holds a strong reference to this closure,
        // and the closure captures self strongly.
        APIClient.shared.fetchData { [weak self] downloadedData in
            // With [weak self], self becomes an optional here, so we must unwrap it.
            // If self has been deallocated, it will be nil.
            guard let self = self else { return }
            self.data = downloadedData
            print("DataDownloader received data: \(self.data ?? "nil")")
        }
    }
}

var downloader: DataDownloader? = DataDownloader()
downloader?.startDownload()

// Allow time for the simulated network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("Setting downloader to nil...")
    downloader = nil // If no weak self, DataDownloader would not deinitialize here.
    print("Finished setting downloader to nil.")
    // Expected output IF WEAK SELF IS USED: DataDownloader deinitialized
}

// Output with [weak self]:
// Setting downloader to nil...
// DataDownloader deinitialized
// DataDownloader received data: Data received!
// Finished setting downloader to nil.

// Output WITHOUT [weak self] (retain cycle):
// Setting downloader to nil...
// Finished setting downloader to nil. (No deinit message)

When to use weak vs. unowned:

  • weak: Use weak when the captured instance might become nil at some point during the closure's lifetime. The weak reference becomes optional (self? or needs guard let self = self else { ... }). This is suitable for delegates, observers, and asynchronous operations where the capturing object might be deallocated before the closure executes.
  • unowned: Use unowned when you know for sure that the captured instance will never be nil during the closure's lifetime. An unowned reference is a non-optional type and is assumed to always have a value. 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 closure and the instance it captures always have the same lifetime, or the captured instance has a longer lifetime. A common scenario is callback closures for UI elements where the element is guaranteed to exist as long as the closure is active.

General Rule of Thumb: If in doubt, use weak. It's safer because it gracefully handles the instance becoming nil.

Practical Scenarios for Weak References

You'll encounter situations requiring weak references frequently in iOS/macOS development:

  1. Delegation Patterns: The delegate property in the delegation pattern (e.g., UITableViewDelegate, custom delegates) should almost always be weak. The delegating object (e.g., UITableView) generally doesn't own its delegate (UIViewController), and the delegate (UIViewController) likely owns the delegating object (UITableView). Using a weak reference prevents a strong reference cycle.

    swift
    protocol NetworkManagerDelegate: AnyObject { // 'AnyObject' required for 'weak var'
        func didCompleteFetch(data: Data)
    }
    
    class NetworkManager {
        weak var delegate: NetworkManagerDelegate?
    
        func fetchData() {
            // ... network request ...
            delegate?.didCompleteFetch(data: Data())
        }
    }
    
    class ViewController: UIViewController, NetworkManagerDelegate {
        let manager = NetworkManager()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            manager.delegate = self // 'self' is the strong owner, 'delegate' reference is weak
            manager.fetchData()
        }
    
        func didCompleteFetch(data: Data) {
            print("Data received in ViewController!")
        }
    }
    
  2. Notification Observers (pre-iOS 9/macOS 10.11 or when using block-based observers): While NotificationCenter in modern APIs often handles this, if you're working with older APIs or building custom notification patterns, be mindful. If an object adds itself as an observer and the observer closure captures the object strongly, and the notification center also holds a strong reference, you have a cycle. Block-based NotificationCenter methods introduced in iOS 4 (later addObserver(forName:object:queue:using:)) can create retain cycles if [weak self] isn't used.

  3. Parent-Child Relationships (where child shouldn't own parent): In data models, if a child object has a reference back to its parent, this parent reference should often be weak if the parent already strongly owns the child. For example, a Folder might have strong references to its File objects, but each File's parentFolder property should be weak to prevent a cycle.

By carefully considering ownership and lifetimes, you can effectively apply weak references to prevent most common memory leaks.

Debugging Memory Leaks

Xcode provides powerful tools to help you identify and debug memory leaks, including strong reference cycles:

  • Memory Graph Debugger: This is your primary tool. While running your app in Xcode, navigate to Debug > Debug Workflow > View Memory Graph Hierarchy (or click the '...' button in the debug nav bar at the bottom and select 'Debug Memory Graph'). This tool visually displays the memory graph of your objects, showing all strong references between them. You can often spot cycles easily here. Look for objects that you expect to be deallocated but still appear in the graph, especially if there's a circular dependency.
  • Instruments (Leaks template): For more in-depth analysis over time, the Leaks instrument can detect true memory leaks by identifying objects that are never deallocated but are unreachable from your application's root.

By combining these tools with a solid understanding of weak and unowned references, you can build memory-efficient Swift applications.

Automatic Reference Counting (ARC) handles everything.

Strengthening Your Memory Management Skills

THE MYTH or PROBLEM: Automatic Reference Counting (ARC) handles everything.

While ARC automates vast parts of memory management, it cannot resolve 'strong reference cycles' where two or more objects hold strong references to each other, preventing their deallocation and leading to memory leaks.

swift
class Controller { var model: ViewModel? }
class ViewModel { var controller: Controller? }

WHAT HAPPENS INTERNALLY? (Reference Counts)

ARC counts strong references. When a strong reference count reaches zero, the object is deallocated. In a cycle, counts never reach zero.

APP ROOT (entry point)
MyViewController (holding strong ref to MyModel)
MyModel (holding strong ref to MyViewController)
1

1. Object Creation

Object A (refCount = 1), Object B (refCount = 1).

2

2. Strong References Formed

Object A references B, Object B references A. A (refCount = 2), B (refCount = 2).

3

3. External References Released

Variables holding A and B are set to `nil`. A (refCount = 1), B (refCount = 1). Neither deallocates!

Visualized execution hierarchy.

Powerful Guarantees

Automatic `nil` Assignment

When an object holding a `weak` reference is deallocated, ARC automatically sets the `weak` reference to `nil`, preventing dangling pointers.

No Strong Hold

A `weak` reference does not increase the reference count of the instance it refers to, thus it doesn't prevent deallocation.

REAL PRODUCTION EXAMPLE: Closure Retain Cycle in a Network Service

A `ViewController` creates a `NetworkService` instance. The `NetworkService` has a completion handler closure that strongly captures `self` (the `ViewController`). The `ViewController` strongly owns the `NetworkService`. This creates a strong reference cycle. The `ViewController` won't deallocate when dismissed.

Impact / Results
Memory leakage: ViewController, NetworkService, and all their properties remain in memory.
App performance degradation over time.
Crashes due to exceeding memory limits.
THE FIX: Using `[weak self]` in Capture Lists
swift
import UIKit

class DataService {
    func fetchData(completion: @escaping (String) -> Void) {
        // Simulating async network call
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            completion("Fetched Data")
        }
    }
}

class MyViewController: UIViewController {
    let service = DataService()

    deinit {
        print("MyViewController is deinitialized")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        fetchDataAndHandle()
    }

    func fetchDataAndHandle() {
        service.fetchData { [weak self] data in // <--- THE FIX: [weak self]
            guard let self = self else { return } // Safely unwrap self
            self.updateUI(with: data)
        }
    }

    private func updateUI(with data: String) {
        print("UI updated with: \(data)")
    }
}

// Example usage to test:
var vc: MyViewController? = MyViewController()

// Present/use the VC (e.g., in a navigation stack)
// ...

// Dismiss/release the VC after a delay to allow service callback:
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("Releasing MyViewController...")
    vc = nil // If [weak self] wasn't used, deinit would not be called here.
    print("MyViewController released.")
}

INTERVIEW PERSPECTIVE

Common Question

“Explain a scenario where you would use `weak` vs. `unowned` references.”

Strong Answer

A strong answer would highlight that `weak` is for when the reference might become `nil` (e.g., delegates, optional parent-child relationships, async callbacks where the capturing object might vanish). `unowned` is for when you are certain the referenced object will always exist as long as the referencing object/closure does (e.g., a child object always having an `unowned` reference to its guaranteed-to-exist parent). Emphasize safety (`weak` doesn't crash) vs. performance/convenience (`unowned` provides non-optional access with a risk of crash if used incorrectly).

Interviewers Expect you to understand:
  • Understand `weak` for optional relationships/shorter lifetimes.
  • Understand `unowned` for non-optional relationships/equal or longer lifetimes.
  • Awareness of runtime crash risk with `unowned`.
  • Knowledge of common patterns (delegates, closures).
KEY TAKEAWAY

Always consider ownership and object lifetimes. Use `weak` or `unowned` to break strong reference cycles and prevent memory leaks, especially with closures and delegates. `weak` is safer when in doubt.

Common Interview Questions

What is the main difference between 'weak' and 'unowned' references?

`weak` references are optional and can become `nil` if the instance they refer to is deallocated, making them safe for cases where the referenced object's lifetime is shorter or unknown. `unowned` references are non-optional and assert that the referenced instance will always exist as long as the `unowned` reference itself exists. Using an `unowned` reference that points to a deallocated instance will cause a runtime crash.

Why do weak references need to be 'var' and optional?

A `weak` reference must be a `var` because its value can change to `nil` when the referenced instance is deallocated by ARC. Constants (`let`) cannot change their value once initialized. It must be optional because it's possible for the reference to become `nil`.

When should I use 'unowned' instead of 'weak' in a closure capture list?

You should use `unowned` when you are absolutely certain that the instance captured by the closure will have the same or a longer lifetime than the closure itself. If the captured instance might be deallocated *before* the closure is ever executed or finishes its work, use `weak` to prevent crashes.

Do structs and enums need weak references?

No, `weak` and `unowned` references apply only to class instances. Structs and enums are value types, not reference types. They are copied when passed around, so they don't participate in ARC's reference counting system and cannot form strong reference cycles.

How can I debug a strong reference cycle in my Swift application?

Xcode's Memory Graph Debugger is the most effective tool. Run your app, trigger the scenario you suspect causes a leak, then open the Debug Memory Graph (Debug > Debug Workflow > View Memory Graph Hierarchy). Look for objects that you expect to be gone but still exist, and inspect their incoming and outgoing strong references for circular paths. Instruments' Leaks template can also verify unreleased memory over time.

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