top of page

Swift Study Notes

The Truth About weak self

💡 The most important idea

Using [weak self] is not automatically the safe or senior way to capture self.

The correct capture depends on the lifetime and ownership behaviour your code requires.

Sometimes a closure should not keep an object alive.

Sometimes keeping that object alive is exactly what the closure must do.

Swift developers are frequently taught to add [weak self] whenever a closure references self.

service.loadData { [weak self] result in
    self?.handle(result)
}

This can begin to feel like a safety rule.

A closure captures self, so we add weak.

The compiler stops complaining.

The developer believes a memory leak has been prevented.

However, this is not enough.

Using weak self without understanding the ownership architecture can change the behaviour of the application, cause important work to disappear and conceal mistakes in the object graph.

The real question is not:

“Should I always use weak self?”

The real questions are:

Who owns this closure?

Who owns the object captured by the closure?

Should the closure keep that object alive?

Does this ownership graph form a cycle?

What Does Memory Mean?

When Swift developers discuss an object being stored in memory, they are normally talking about the application’s use of RAM.

RAM is the working memory used by the application while it is running.

Objects, values, closures, images and other runtime information occupy parts of that memory.

When an object is no longer needed, Swift should be able to destroy it and make its memory available for reuse.

For class instances, Swift manages this process using Automatic Reference Counting.

What Is ARC?

ARC stands for Automatic Reference Counting.

ARC manages the lifetime of class instances by tracking the strong references that require each object to remain alive.

A useful mental model is to imagine that every object has a counter.

Strong references to object: 3

When another strong reference begins owning the object, the count increases.

Strong references to object: 4

When a strong reference stops owning the object, the count decreases.

Strong references to object: 3

When the strong reference count reaches zero, nothing in the program strongly requires that object to remain alive.

Strong references to object: 0

Swift can then deinitialise the object and reclaim its memory.

Conceptually:

If no code strongly owns an object, ARC knows that the object is no longer required and can remove it from RAM.

The implementation is more sophisticated than a counter printed beside every object, and the compiler can optimise many ownership operations.

However, the reference-counting model is the correct foundation for understanding ARC.

A Simple ARC Example

final class Rocket {
    let name: String

    init(name: String) {
        self.name = name
        print("\(name) created")
    }

    deinit {
        print("\(name) destroyed")
    }
}

We can create an optional strong reference.

var rocket: Rocket? = Rocket(name: "Explorer")

The variable strongly references the Rocket instance.

rocket ─────▶ Rocket("Explorer")

When we remove that strong reference:

rocket = nil

the Rocket can be destroyed, provided no other strong references exist.

Explorer created
Explorer destroyed

The Three ARC Reference Types

Swift provides three main ways for one reference to point to a class instance:

  • Strong references.
  • Weak references.
  • Unowned references.

Each describes a different ownership relationship.

Strong References

A strong reference keeps an object alive.

Strong references are the default.

let rocket = Rocket(name: "Explorer")

The variable rocket strongly owns the Rocket instance.

As long as that reference remains active, ARC must not destroy the object.

Definition

A strong reference expresses ownership and contributes to the strong reference count of an object.

Strong references are not dangerous.

They are how objects are normally kept alive.

Without strong references, class instances could disappear while the program was still using them.

Weak References

A weak reference points to an object without keeping it alive.

weak var rocket: Rocket?

A weak reference does not increase the object’s strong reference count.

If all strong references disappear, ARC destroys the object.

The weak reference is then automatically set to nil.

Definition

A weak reference observes an object without owning it. It becomes nil automatically when the object is deallocated.

Because a weak reference can become nil, it is normally declared as an optional variable.

weak var delegate: LaunchDelegate?

Unowned References

An unowned reference also points to an object without keeping it alive.

unowned let owner: MissionController

Unlike an ordinary weak reference, a non-optional unowned reference is not automatically set to nil.

It expresses a stronger assumption:

The referenced object will still exist whenever this reference is used.

If that assumption is wrong and the program accesses the unowned reference after the object has been deallocated, the program traps at runtime.

Definition

An unowned reference does not keep an object alive and assumes the object will outlive every use of the reference.

Strong, Weak and Unowned Compared

Reference Keeps Object Alive? Can Become nil? Risk
Strong Yes Only if declared optional and explicitly changed Can participate in a strong reference cycle
Weak No Yes, automatically Expected work may disappear because the object was deallocated
Unowned No Not for the usual non-optional form Runtime trap if accessed after deallocation

Closures Are Reference Types

Closures are reference types in Swift.

A closure can capture values and references from the surrounding scope.

final class LaunchController {
    let missionName = "Explorer"

    func prepare() -> () -> Void {
        {
            print(self.missionName)
        }
    }
}

The returned closure captures self.

By default, that capture is strong.

The closure therefore keeps the LaunchController alive for as long as the closure itself remains alive.

A Strong Capture Is Not Automatically a Cycle

This is one of the most important distinctions to understand.

A closure strongly capturing self does not automatically create a strong reference cycle.

Consider:

final class LaunchController {
    func load() {
        performTemporaryWork {
            self.showLaunch()
        }
    }

    private func showLaunch() {
        print("Launch ready")
    }
}

If performTemporaryWork retains the closure only until the operation completes and then releases it, the ownership may look like this:

Temporary operation
        │
        ▼
     Closure
        │
        ▼
LaunchController

The closure keeps the controller alive while the work is in progress.

When the operation releases the closure, the capture is released.

There is no cycle unless the LaunchController also owns something that leads back to that same closure.

A strong capture is an ownership relationship.

A strong reference cycle requires a closed ownership loop.

What Is a Strong Reference Cycle?

A strong reference cycle occurs when a group of objects and closures strongly retain one another in a loop.

Object A
   │
   ▼
Object B
   │
   ▼
Closure
   │
   └────────▶ Object A

Even when the rest of the application stops using these objects, every object in the cycle still has at least one strong reference.

Their reference counts never reach zero.

ARC therefore cannot destroy them.

A Common Closure Cycle

final class LaunchController {
    var completion: (() -> Void)?

    func configure() {
        completion = {
            self.showLaunch()
        }
    }

    private func showLaunch() {
        print("Launch ready")
    }
}

The LaunchController strongly owns its completion closure.

The closure strongly captures the LaunchController.

LaunchController
       │
       ▼
completion closure
       │
       └──────────▶ LaunchController

This is a strong reference cycle.

Removing the external reference to the controller is not enough.

The controller and closure continue keeping one another alive.

Breaking the Cycle With weak self

final class LaunchController {
    var completion: (() -> Void)?

    func configure() {
        completion = { [weak self] in
            self?.showLaunch()
        }
    }

    private func showLaunch() {
        print("Launch ready")
    }
}

The controller strongly owns the closure.

The closure now holds only a weak reference back to the controller.

LaunchController
       │
       ▼
completion closure
       │
       ┄ ┄ ┄ ┄ ┄ ▶ LaunchController
              weak

The loop has been broken.

When external strong references to the controller disappear, the controller can be destroyed.

The closure’s weak reference becomes nil.

Why weak self Is Not Automatically Safe

The word weak sounds safe because it can prevent a memory leak.

However, weak references change lifetime behaviour.

A weak reference deliberately allows the captured object to disappear.

This can be exactly what we need.

It can also break the work we intended to complete.

networking.loadData { [weak self] data in
    self?.save(data)
}

If self is deallocated before the response arrives, the save operation never happens.

The closure runs.

The optional reference is nil.

The optional call silently does nothing.

Using weak self chooses this behaviour:

If the object disappears before the closure runs, abandon the work that required that object.

That may be correct for updating a screen that is no longer visible.

It may be completely wrong for saving a payment, recording a completed upload or finishing a required model operation.

Weak Is a Lifetime Decision

When you write:

{ [weak self] in
    self?.updateInterface()
}

you are not simply avoiding a leak.

You are saying:

This closure does not own self.

If self no longer exists when the closure runs, skip this operation.

That is a design decision.

It should be made deliberately.

Strong Captures Can Be Correct

Sometimes the closure must keep the captured object alive until the operation completes.

final class ImportOperation {
    private let importer: DataImporter

    init(importer: DataImporter) {
        self.importer = importer
    }

    func begin() {
        importer.importData {
            self.finishImport()
        }
    }

    private func finishImport() {
        print("Import completed")
    }
}

Suppose nothing else retains the ImportOperation after begin() returns.

The strong capture inside the completion closure may intentionally keep the operation alive until the import finishes.

If we automatically change it to:

importer.importData { [weak self] in
    self?.finishImport()
}

the ImportOperation may be destroyed before the callback arrives.

finishImport() may never run.

There may be no leak in either version.

The strong capture may simply be the mechanism that keeps the operation alive for its required lifetime.

The Question Is Not Strong or Weak

The correct question is:

Should this closure own the captured object until the closure is released?

If the answer is yes, a strong capture may be correct.

If the answer is no, a weak or unowned capture may be appropriate.

After that, inspect the ownership graph and determine whether a cycle exists.

An API Manager That Stores Callbacks

Consider an APIManager that allows several parts of the application to request the same data.

While a request is running, the manager stores callbacks from interested objects.

When the network response arrives, the manager fires every callback.

import Foundation

final class APIManager {

    typealias DataCallback = (Result<Data, Error>) -> Void

    private var callbacks: [DataCallback] = []
    private var isRequestInProgress = false

    func requestData(
        completion: @escaping DataCallback
    ) {
        callbacks.append(completion)

        guard !isRequestInProgress else {
            return
        }

        isRequestInProgress = true
        makeNetworkingRequestToGetData()
    }

    private func makeNetworkingRequestToGetData() {
        Task {
            do {
                let data = try await downloadData()
                completeRequest(with: .success(data))
            } catch {
                completeRequest(with: .failure(error))
            }
        }
    }

    private func completeRequest(
        with result: Result<Data, Error>
    ) {
        let callbacksToFire = callbacks

        callbacks.removeAll()
        isRequestInProgress = false

        for callback in callbacksToFire {
            callback(result)
        }
    }

    private func downloadData() async throws -> Data {
        // Placeholder for the real network request.
        Data()
    }
}

The manager stores escaping closures in an array.

private var callbacks: [DataCallback] = []

Each closure remains alive until the manager removes it.

What Does the Manager Own?

The APIManager strongly owns its callback array.

The callback array strongly owns each closure.

APIManager
    │
    ▼
callbacks array
    │
    ▼
callback closure

If a callback captures an object strongly, the chain continues.

APIManager
    │
    ▼
callback closure
    │
    ▼
LaunchModel

This means registering a callback can keep the LaunchModel alive.

That may or may not be correct.

A Consumer That Strongly Captures Itself

final class LaunchModel {

    private let apiManager: APIManager
    private(set) var data: Data?

    init(apiManager: APIManager) {
        self.apiManager = apiManager
    }

    func load() {
        apiManager.requestData { result in
            switch result {
            case .success(let data):
                self.data = data

            case .failure:
                self.data = nil
            }
        }
    }
}

The callback strongly captures the LaunchModel.

The APIManager stores that callback.

The resulting graph is:

LaunchModel
    │
    ▼
APIManager
    │
    ▼
callback closure
    │
    └────────────▶ LaunchModel

This is a strong reference cycle while the callback remains stored.

The LaunchModel strongly owns the APIManager.

The APIManager strongly owns the closure.

The closure strongly owns the LaunchModel.

A Temporary Cycle Is Still a Cycle

In this example, the APIManager removes the callbacks after the request completes.

callbacks.removeAll()

Removing the closures breaks the cycle.

The LaunchModel can then be destroyed if no other strong references remain.

This means the cycle may be temporary rather than permanent.

Temporary cycles are not always memory leaks.

They can intentionally keep participating objects alive until an operation finishes.

This is why architecture matters.

You need to know whether the callback is released after one response, stored forever, cancelled, replaced or retained by a long-lived singleton.

What Happens If the APIManager Is a Singleton?

Suppose the manager exists for the entire lifetime of the application.

final class APIManager {
    static let shared = APIManager()

    // ...
}

If callbacks are never removed, the manager may keep every captured consumer alive forever.

APIManager.shared
      │
      ▼
callback closure
      │
      ▼
old screen model
      │
      ▼
view-related state

A screen may have disappeared visually, but its model remains in RAM because the manager still owns its callback.

This is a genuine leak caused by the callback-storage architecture.

Adding [weak self] may prevent the model from being retained, but it does not fix the manager’s endlessly growing callback array.

The manager still stores unused closures.

A correct design may also need cancellation tokens, callback removal or one-shot callback cleanup.

The Weak Capture Version

final class LaunchModel {

    private let apiManager: APIManager
    private(set) var data: Data?

    init(apiManager: APIManager) {
        self.apiManager = apiManager
    }

    func load() {
        apiManager.requestData { [weak self] result in
            guard let self else {
                return
            }

            switch result {
            case .success(let data):
                self.data = data

            case .failure:
                self.data = nil
            }
        }
    }
}

The ownership graph becomes:

LaunchModel
    │
    ▼
APIManager
    │
    ▼
callback closure
    │
    ┄ ┄ ┄ ┄ ┄ ▶ LaunchModel
              weak

The cycle has been removed.

If the LaunchModel is no longer strongly owned elsewhere, it can be deallocated while the request is still running.

When the callback fires, self may be nil.

The result is ignored.

When Weak Is Correct

This behaviour is often correct for a screen model.

Imagine that the user opens a launch screen and begins downloading data.

Before the response arrives, the user closes the screen.

If the model existed only to support that screen, there may be no reason to keep it alive.

The callback can safely do nothing after the model has disappeared.

apiManager.requestData { [weak self] result in
    guard let self else {
        return
    }

    self.handle(result)
}

Here, weak capture aligns with the intended lifetime.

When Weak Can Break the Behaviour

Now imagine that the callback is responsible for completing an important operation.

final class UploadCoordinator {

    private let apiManager: APIManager

    init(apiManager: APIManager) {
        self.apiManager = apiManager
    }

    func upload() {
        apiManager.requestData { [weak self] result in
            self?.persistUploadResult(result)
        }
    }

    private func persistUploadResult(
        _ result: Result<Data, Error>
    ) {
        print("Saving upload result")
    }
}

If no other object retains UploadCoordinator, it may be deallocated immediately after upload() returns.

The request may complete successfully.

The callback may fire.

However, self is now nil, so the result is never persisted.

The weak capture avoided retaining the coordinator.

It also prevented the coordinator from completing its responsibility.

A Strong Capture May Be the Required Design

func upload() {
    apiManager.requestData { result in
        self.persistUploadResult(result)
    }
}

The strong capture ensures that UploadCoordinator remains alive while the APIManager stores the callback.

When the manager fires and removes the callback, the strong capture is released.

If there are no other owners, the coordinator can then be deallocated.

This can be entirely correct.

The closure is providing temporary ownership for the duration of the operation.

The Critical Cleanup Requirement

The strong version depends upon the APIManager releasing the callback.

This line is not a minor implementation detail:

callbacks.removeAll()

It is part of the ownership architecture.

If the manager forgets to release completed callbacks, every strongly captured consumer may remain alive.

If the manager guarantees one-shot cleanup, the temporary retention may be exactly what the operation needs.

Do Not Hide Architecture Behind self?

This code is common:

{ [weak self] result in
    self?.handle(result)
}

It is concise.

However, it can make an important event invisible.

If self has disappeared, the operation is silently discarded.

Sometimes that is acceptable.

Sometimes it should be investigated.

During development, an explicit guard can make the decision easier to see.

{ [weak self] result in
    guard let self else {
        return
    }

    self.handle(result)
}

The outcome is still the same, but the code makes it clearer that the callback deliberately abandons its work when the owner no longer exists.

Weak Self Does Not Make the Closure Non-Escaping

A capture list does not change whether a closure escapes.

The closure still escapes if it is stored or used after the function returns.

func requestData(
    completion: @escaping DataCallback
)

The capture list changes how values from the surrounding scope are captured.

{ [weak self] in
    self?.handleResult()
}

The closure still exists.

It still consumes memory.

It still remains stored by whoever owns it.

Only its reference to self has become weak.

Weak Self Does Not Fix Every Leak

Consider an APIManager that never clears its callbacks.

final class APIManager {
    private var callbacks: [DataCallback] = []

    func requestData(
        completion: @escaping DataCallback
    ) {
        callbacks.append(completion)
    }
}

Consumers may use weak captures.

apiManager.requestData { [weak self] result in
    self?.handle(result)
}

The consumer can now deallocate.

However, the APIManager continues storing every callback closure forever.

The closure may contain only a weak reference, but the closure itself still occupies memory.

The callback array continues growing.

This remains a memory-management bug.

Weak self can break one strong edge.

It does not automatically repair the complete ownership architecture.

One-Shot Callbacks Should Be Released

If a callback should run only once, remove it once the result has been delivered.

private func completeRequest(
    with result: Result<Data, Error>
) {
    let callbacksToFire = callbacks

    callbacks.removeAll()
    isRequestInProgress = false

    for callback in callbacksToFire {
        callback(result)
    }
}

Copying the callbacks into a local constant allows the manager to clear its stored references before invoking user code.

This can also reduce the risk of unexpected re-entrancy interacting with the old callback storage.

Long-Lived Subscriptions Need Cancellation

Some callbacks are not one-shot completions.

They are subscriptions intended to receive repeated values.

For those callbacks, automatic removal after the first event would be incorrect.

The architecture needs an explicit way to unsubscribe.

struct ObservationToken: Hashable {
    let id: UUID
}
final class APIManager {

    typealias DataCallback = (Data) -> Void

    private var callbacks: [
        ObservationToken: DataCallback
    ] = [:]

    func observeData(
        _ callback: @escaping DataCallback
    ) -> ObservationToken {
        let token = ObservationToken(id: UUID())
        callbacks[token] = callback
        return token
    }

    func removeObserver(
        _ token: ObservationToken
    ) {
        callbacks[token] = nil
    }
}

The consumer must then decide when the subscription ends.

The ownership design cannot be replaced by habitually adding weak self.

Using Weak With a Long-Lived Subscription

final class LaunchModel {

    private let apiManager: APIManager
    private var observationToken: ObservationToken?

    init(apiManager: APIManager) {
        self.apiManager = apiManager
    }

    func beginObserving() {
        observationToken = apiManager.observeData {
            [weak self] data in

            self?.handle(data)
        }
    }

    deinit {
        if let observationToken {
            apiManager.removeObserver(observationToken)
        }
    }

    private func handle(_ data: Data) {
        print(data)
    }
}

The weak capture prevents the callback from owning the model.

The explicit removal prevents the APIManager from retaining an obsolete closure forever.

Both parts matter.

Using unowned self

A closure can capture self as unowned.

{ [unowned self] in
    self.showLaunch()
}

This avoids a strong capture and avoids optional access.

However, it makes a strict lifetime promise.

The code promises that self will still exist whenever the closure executes.

If the closure runs after self has been deallocated, the application traps.

When Unowned Can Be Appropriate

Unowned is appropriate when the ownership architecture guarantees the referenced object will outlive the closure’s use.

For example, an object may own a closure that is guaranteed never to escape the object’s lifetime, while the closure refers back to that object.

final class Formatter {

    lazy var formattedName: () -> String = {
        [unowned self] in

        self.makeFormattedName()
    }

    private func makeFormattedName() -> String {
        "Formatted"
    }
}

The closure is owned by the Formatter.

It is intended to be used only while the Formatter exists.

The unowned capture prevents a cycle without introducing optional handling.

However, this remains a promise that must be supported by the architecture.

Weak vs Unowned

Use a weak reference when the referenced object may legitimately disappear before the reference is used.

[weak self]

Use an unowned reference when the referenced object must still exist whenever the reference is used.

[unowned self]
Weak Unowned
The object may already be gone. The object is expected to still exist.
Normally accessed as an optional. Normally accessed as non-optional.
Becomes nil after deallocation. Traps if accessed after deallocation.
Failure can be handled or ignored. A broken lifetime promise is fatal.

Capturing Only the Value You Need

Sometimes a closure does not need to capture the entire object.

It can capture one immutable value instead.

final class LaunchController {

    let missionID: String

    init(missionID: String) {
        self.missionID = missionID
    }

    func begin() {
        let missionID = missionID

        performWork {
            print("Mission: \(missionID)")
        }
    }
}

The closure captures the String rather than the LaunchController.

This can simplify ownership when the closure does not need the object’s identity or mutable state.

Capture lists can make this explicit.

performWork { [missionID] in
    print("Mission: \(missionID)")
}

A Capture List Captures a Snapshot

Capture-list entries are evaluated when the closure is created.

var missionName = "Explorer"

let printMission = { [missionName] in
    print(missionName)
}

missionName = "Voyager"

printMission() // Explorer

The closure captured the value of missionName at creation time.

This is different from strongly or weakly capturing a reference to a mutable object.

Temporary Strong Capture From Weak Self

A common pattern begins with a weak capture and then creates a temporary strong reference for the duration of the callback.

apiManager.requestData { [weak self] result in
    guard let self else {
        return
    }

    self.process(result)
    self.updateCache()
    self.finishLoading()
}

The capture from the closure to self is weak.

Once the guard succeeds, the local self is strong for the rest of that closure execution.

This prevents the object from disappearing halfway through the callback.

After the closure invocation completes, the temporary strong reference is released.

Do Not Guess About Escaping Closure Lifetimes

Before choosing a capture, determine how the closure is stored.

Ask:

  • Is the closure called immediately?
  • Is it stored in a property?
  • Is it stored in a collection?
  • Is it released after one call?
  • Can it be called repeatedly?
  • Can the operation be cancelled?
  • Is the owner a singleton?
  • Does the captured object also own the closure owner?

These questions reveal whether there is a cycle and whether the closure should extend the object’s lifetime.

The Architecture Determines the Capture

Consider four possible situations.

1. A Temporary Completion That Should Keep the Worker Alive

service.performWork {
    self.finishRequiredOperation()
}

A strong capture may be correct.

2. A Screen Callback That Is Irrelevant After Dismissal

service.loadData { [weak self] result in
    self?.display(result)
}

A weak capture may be correct.

3. A Closure Owned by the Same Object It Captures

callback = { [weak self] in
    self?.performAction()
}

A weak or unowned capture may be needed to break the cycle.

4. A Lifetime That Is Guaranteed by Design

callback = { [unowned self] in
    self.performAction()
}

An unowned capture may be appropriate when the guarantee is genuine.

The Wrong Habit

The following reasoning is not sufficient:

“This is an escaping closure, so I should use weak self.”

An escaping closure can capture strongly without leaking.

A non-escaping closure generally does not need weak capture because it cannot outlive the function call that receives it.

The relevant question is the ownership graph, not merely whether the closure escapes.

The Better Habit

“This closure is stored by the APIManager until the request finishes. The model owns the APIManager. If the closure strongly captures the model, there is a temporary cycle. The manager clears the callback after completion, so that cycle is broken. Do I want the callback to keep the model alive until then?”

That is architectural reasoning.

It is much more valuable than automatically inserting a capture list.

A Complete APIManager Example

import Foundation

final class APIManager {

    typealias DataCallback = (
        Result<Data, Error>
    ) -> Void

    private var callbacks: [DataCallback] = []
    private var isRequestInProgress = false

    func requestData(
        completion: @escaping DataCallback
    ) {
        callbacks.append(completion)

        guard !isRequestInProgress else {
            return
        }

        isRequestInProgress = true
        makeNetworkingRequestToGetData()
    }

    private func makeNetworkingRequestToGetData() {
        Task {
            let result: Result<Data, Error>

            do {
                let data = try await downloadData()
                result = .success(data)
            } catch {
                result = .failure(error)
            }

            completeRequest(with: result)
        }
    }

    private func completeRequest(
        with result: Result<Data, Error>
    ) {
        let callbacksToFire = callbacks

        callbacks.removeAll()
        isRequestInProgress = false

        for callback in callbacksToFire {
            callback(result)
        }
    }

    private func downloadData() async throws -> Data {
        try await Task.sleep(
            for: .milliseconds(500)
        )

        return Data("Rocket data".utf8)
    }
}

Several consumers can request data before the network operation completes.

apiManager.requestData { result in
    print("First consumer:", result)
}

apiManager.requestData { result in
    print("Second consumer:", result)
}

The APIManager performs one request and stores both callbacks.

When the response arrives, it sends the same result to every interested consumer.

Strong Consumer Example

final class LaunchDownloadCoordinator {

    private let apiManager: APIManager
    private(set) var downloadedData: Data?

    init(apiManager: APIManager) {
        self.apiManager = apiManager
        print("Coordinator created")
    }

    deinit {
        print("Coordinator destroyed")
    }

    func begin() {
        apiManager.requestData { result in
            switch result {
            case .success(let data):
                self.downloadedData = data
                print("Coordinator stored the data")

            case .failure(let error):
                print(error)
            }
        }
    }
}

The callback strongly captures the coordinator.

That capture keeps the coordinator alive while the APIManager stores the callback.

After the callback is fired and removed, the coordinator can be released.

This may be the intended behaviour.

Weak Consumer Example

final class LaunchScreenModel {

    private let apiManager: APIManager
    private(set) var downloadedData: Data?

    init(apiManager: APIManager) {
        self.apiManager = apiManager
        print("Screen model created")
    }

    deinit {
        print("Screen model destroyed")
    }

    func load() {
        apiManager.requestData { [weak self] result in
            guard let self else {
                print("Screen disappeared before response")
                return
            }

            switch result {
            case .success(let data):
                downloadedData = data

            case .failure:
                downloadedData = nil
            }
        }
    }
}

If the user leaves the screen, the model can be destroyed before the request completes.

The callback does not prevent that destruction.

This may be the intended behaviour because the downloaded result is no longer needed by that screen.

Neither Version Is Universally Better

Strong Capture Weak Capture
Keeps the captured object alive. Allows the captured object to disappear.
May be required to complete important work. May be appropriate when work becomes irrelevant.
Can participate in a cycle. Breaks the strong edge back to the object.
Requires understanding when the closure is released. Requires accepting that the callback may do nothing.

How Much Damage Can the Wrong Choice Cause?

Using the wrong capture semantics can produce two opposite categories of failure.

Retaining Too Much

A strong capture can keep screens, models, images and other object graphs alive after they should have been released.

This increases RAM usage and can eventually affect performance or cause the operating system to terminate the app.

Retaining Too Little

A weak capture can allow an operation coordinator or model to disappear before its work is complete.

The callback then silently skips important behaviour.

Data may not be saved.

State may never be updated.

A transaction may not finish its final step.

A test may fail intermittently because an object’s lifetime was not guaranteed.

Memory Leaks and Behavioural Bugs

Developers often treat memory leaks as the only danger.

However, lifetime bugs can appear in both directions.

Object lives too long
        │
        ▼
Memory leak or unnecessary retention
Object dies too early
        │
        ▼
Required callback work never happens

Good ARC design keeps objects alive for exactly as long as their responsibilities require.

Do Not Be Ignorant of the Ownership Graph

You cannot safely choose between strong, weak and unowned without understanding the architecture.

You need to know:

  • Which object owns the APIManager.
  • Whether the APIManager is shared or temporary.
  • How long callbacks remain stored.
  • Whether callbacks are removed after execution.
  • Whether the consumer needs to survive until completion.
  • Whether the consumer owns the object storing the closure.
  • Whether the work may be abandoned safely.

Without these answers, adding [weak self] is only hoping for the best.

A More Senior Mental Model

A junior explanation may be:

“Use weak self to prevent retain cycles.”

A more complete explanation is:

“Closures strongly capture referenced objects by default. I need to inspect who retains the closure and whether the captured object owns something that leads back to it. If that creates a cycle, I must break an ownership edge or ensure the closure is released. I then choose strong, weak or unowned according to the lifetime the operation requires.”

That is the truth about weak self.

Questions to Ask Before Writing a Capture List

  1. Who stores this closure?
  2. How long will it be stored?
  3. Does self own the closure owner?
  4. Would a strong capture complete an ownership loop?
  5. Should the closure keep self alive?
  6. What should happen if self disappears first?
  7. Is the lifetime guarantee strong enough for unowned?
  8. Who removes or cancels the callback?

Interview Questions

What is ARC?

Automatic Reference Counting is Swift’s system for managing the lifetime of class instances according to their strong ownership references.

When is a class instance deallocated?

A class instance can be deallocated when no strong references to it remain.

What is a strong reference?

A strong reference owns an object and keeps it alive.

What is a weak reference?

A weak reference points to an object without keeping it alive and becomes nil when that object is deallocated.

What is an unowned reference?

An unowned reference does not keep an object alive and assumes that the object will still exist whenever the reference is accessed.

What happens when an invalid unowned reference is accessed?

The application traps at runtime because the referenced object has already been deallocated.

Do closures capture self strongly by default?

Yes.

When a closure references a class instance, it normally captures that instance strongly unless the capture list specifies another relationship.

Does strongly capturing self always create a retain cycle?

No.

A cycle exists only when the ownership relationships form a closed loop.

Why can weak self break application behaviour?

Because the weak capture allows the object to be deallocated before the closure runs.

If the callback depends on that object, the required work may be skipped.

When is weak self appropriate?

It is appropriate when the closure should not extend the object’s lifetime and the work can safely be abandoned if the object no longer exists.

When can a strong capture be appropriate?

A strong capture is appropriate when the closure should keep the object alive until the operation completes and the resulting ownership graph does not create an unbroken permanent cycle.

When is unowned self appropriate?

It is appropriate when the architecture guarantees that self will still exist every time the closure is called.

Does weak self remove the callback from its owner?

No.

The callback owner must still release or remove the closure according to the architecture.

Can a temporary strong reference cycle be released?

Yes.

If an owner removes the stored closure after completion, the cycle is broken and the involved objects can be deallocated.

What does memory mean in this discussion?

It normally refers to RAM used by the running application.

Final Revision Note

weak self is not the automatically safe version of capturing self.

It is one ownership choice.

A strong reference keeps an object alive.

A weak reference observes an object without keeping it alive and becomes nil when the object disappears.

An unowned reference does not keep an object alive and assumes the object will still exist whenever it is used.

ARC can reclaim a class instance when its strong reference count reaches zero.

A strong reference cycle prevents that count from reaching zero because the objects or closures continue owning one another.

The correct capture depends on the ownership graph and the required lifetime of the work.

Remember

Do not ask:

“Should I use weak self because this is a closure?”

Ask:

“Who owns this closure, should it keep self alive, and does this ownership graph form a cycle?”

Understanding those answers is safer than adding [weak self] and hoping for the best.

bottom of page