Articles
Swift Memory Management Interview Questions: Retain Cycles, Closures, and Tasks
Prepare Swift memory management interviews with retain-cycle graphs, weak and unowned captures, task lifetimes, cancellation, and release verification.
A screen disappears, but its view model remains alive. One developer adds [weak self] to every closure. Another waits for the network request to finish and sees the object released. They may be investigating different problems: a permanent ownership cycle, a long-lived task, or intentional retention by a cache.
Swift memory management interview questions become clearer when you draw who owns what and identify the event that should end each lifetime. Capture syntax is a tool for implementing that design, not a substitute for it.
This guide follows a stored-closure example, an async task ownership trace, and a release-verification plan. Official language facts come from the Swift book and Apple documentation. Our inference and recommendations explain how to diagnose lifetime problems and choose defensible fixes.
Start with Explain iOS ARC and avoid retain cycles, then practice answering with an ownership graph before naming weak or unowned.
What does ARC manage, and what does it not decide?
Automatic Reference Counting manages the lifetime of class instances through their references. A strong reference keeps an instance alive. A cycle of strong references can prevent the instances from becoming releasable after external owners disappear. The Swift book's ARC chapter is the language reference for these relationships.
ARC does not know that a particular screen is no longer useful to the user. If a navigation coordinator, callback registry, task, or cache still owns its model, the reference graph can legitimately retain it. Whether that retention is a bug depends on the intended lifetime.
Also separate object lifetime from process memory totals. Allocators can retain freed storage for reuse, and caches can deliberately hold objects. A flat or elevated process footprint does not identify a retain cycle by itself. Look for surviving instances and the references that explain them.
How do you prove a stored closure creates a cycle?
Use a small example where a class stores a closure and that closure captures the same instance strongly:
final class Panel {
var title = "Details"
var onRefresh: (() -> Void)?
func connect() {
onRefresh = { print(self.title) }
}
}
The graph is Panel → onRefresh closure → Panel. If the last external owner releases the panel, the two strong edges remain. The important condition is that the closure is stored for a lifetime that creates the return path. Merely seeing self inside a closure is not enough to prove a leak.
One possible repair is a weak capture:
func connect() {
onRefresh = { [weak self] in
guard let self else { return }
print(self.title)
}
}
Another is an explicit lifecycle action that clears onRefresh when the callback is no longer needed. These approaches express different contracts. Weak capture allows the callback to become a no-op after the panel disappears. Explicit clearing can be appropriate when the owner controls registration and deregistration reliably.
Before choosing, ask whether silently doing nothing is acceptable. If the callback must finish an operation even after the screen closes, giving the operation an independent owner may be clearer than weakening a reference and dropping required work.
When should you use weak versus unowned?
A weak reference does not keep the referenced instance alive and becomes nil when the instance is deallocated. An unowned reference also avoids strong ownership, but accessing it after the referenced instance has gone away is invalid. The choice must follow a lifetime guarantee you can explain.
| Relationship | Useful starting point | Required reasoning |
|---|---|---|
| Delegate may disappear first | Weak reference | Caller handles an absent delegate |
| Callback may run after screen dismissal | Weak capture or separate operation owner | Define whether the result may be discarded |
| Referenced object is guaranteed to outlive the reference | Unowned may fit | Demonstrate the guarantee on every execution path |
| Operation must keep an object alive until completion | Strong reference may be correct | Define completion and cancellation ownership |
Do not choose unowned merely to avoid optional syntax. In an interview, change the timing: the screen closes before the callback executes. If the lifetime argument collapses, unowned was hiding an assumption rather than proving it.
Similarly, making every relationship weak can cause premature deallocation. A child object with no strong owner can vanish immediately. Correct memory management needs enough ownership to keep required work alive and a clear way to end that ownership.
Does a capture list always read the latest state?
Capture semantics affect behavior as well as retention. Capturing an object's property value can preserve the value from closure creation, while capturing the object allows reading its current property later. Value types and references inside them deserve separate reasoning.
For a refresh callback, decide whether it should use the title that existed when the callback was registered or the title at execution time. A memory fix that changes that behavior can introduce a stale-state bug even while removing a cycle.
Capturing only a needed dependency can reduce the graph, but inspect that dependency's ownership too. If it strongly points back to the parent, the cycle may still exist through another route. Draw actual edges instead of treating a shorter capture list as automatic proof.
Is a Task that retains self necessarily a leak?
No. A finite task can intentionally retain an object while it performs work and release that capture when it completes. A task awaiting a long operation can therefore extend lifetime without constituting a permanent cycle.
The harder case is work that never finishes, such as an indefinite event stream. If the task needs the model for the entire loop, the model can remain alive indefinitely. Storing the task handle on the model and relying only on the model's deinitializer to cancel it can create a lifecycle dead end: the event needed to cancel may never arrive because the task keeps the model alive.
Apple's Task documentation and the Swift book's concurrency chapter describe task behavior and cooperative cancellation. A task handle is a means to manage work; dropping the handle is not the same as cancelling the task.
A stronger design gives a visible owner responsibility for starting and stopping observation. For example, a screen lifecycle or coordinator can call stopObserving before releasing the model. That action should cancel work and detach registrations, with the actual async operation cooperating in termination.
Why can weak self still stay alive across await?
A weak capture only describes the captured reference. If the closure immediately promotes it to a strong local and then awaits a long-running instance method, that strong ownership can extend across the suspension and method execution.
The shape Task { [weak self] in guard let self else { return }; await self.observeForever() } is therefore not a general solution for an observation task that should stop when the screen disappears. The guard makes the instance available strongly for the work that follows.
Possible designs include processing one event at a time with a deliberately bounded strong lifetime, moving the observation loop into a separate service, or explicitly cancelling at the owning lifecycle boundary. The correct choice depends on whether the model should survive between events and who must retain the subscription.
Do not promise that a weak check on every iteration guarantees prompt release if an instance method itself holds the object during a long suspension. Explain the entire call chain. This is an ownership question that syntax alone cannot settle.
What should a cancellation lifecycle specify?
The Swift concurrency model uses cooperative cancellation. Calling cancel communicates that the result is no longer wanted; the task must reach cancellation-aware work or check cancellation and respond. An operation that ignores cancellation can continue retaining its inputs.
| Lifecycle event | Owner responsibility | Evidence to inspect |
|---|---|---|
| Screen starts observing | Create one observation task | No duplicate subscription |
| Screen leaves its active role | Cancel and detach owned work | Cancellation reaches the operation |
| Operation receives cancellation | Exit or perform bounded cleanup | Loop stops and captures are released |
| Late result arrives | Respect lifetime or request identity | Dismissed UI is not updated |
| Screen opens again | Start a fresh valid lifecycle | No accumulating old models |
Clearing a stored handle can release the model's reference to the handle, but it does not substitute for making the task finish. Conversely, cancellation alone does not prove every callback registration was removed. Test both the task and any separate timer, observer token, or delegate relationship it created.
Keep cleanup idempotent. Calling stop twice should not create a new task, resume a continuation twice, or remove another owner's registration. Those are correctness bugs that can appear during the same navigation race as the memory issue.
How do timers, delegates, and observers complicate the graph?
A recurring timer or stored callback can extend the lifetime of its target. An observer token can represent a registration whose callback captures a model. The relevant graph can span framework objects, so it may not be visible in the class's own properties alone.
Inventory each registration: who creates it, who stores the handle, what the callback captures, and which event unregisters it. API families have different ownership rules; do not generalize one notification or timer API's behavior to every observer mechanism.
For a delegate, ask whether it is a back-reference to an owner. If so, weak ownership often expresses the intended relationship. For a dependency that exists only because a model owns it, strong ownership may be necessary. The direction and lifetime contract determine the answer.
Autorelease pools address a different issue: temporary Objective-C objects can accumulate until the pool drains. They do not break a Swift strong-reference cycle. Mention them when profiling identifies temporary allocation pressure, rather than as a universal leak fix.
How would you verify that the repair works?
Create a repeatable sequence: open the screen, start its work, leave it, allow expected cleanup, and repeat. Compare surviving instance counts and inspect the memory graph for the model that should have been released. Apple's memory diagnostics guidance provides the relevant tooling entry point.
A weak reference in a controlled test can help assert that a model becomes nil after its intended lifetime. For asynchronous work, use a deterministic completion or cancellation acknowledgement where possible instead of an arbitrary sleep. Logging deinit can support investigation but should not be the only production-quality evidence.
Test success, failure, cancellation, and an operation that never yields a value. Then test repeated navigation. A fix that releases only after successful networking can still retain objects indefinitely during an outage.
Record what you actually measured. A unit test of a stored closure proves that example's graph; it does not certify an entire app's memory behavior. Likewise, fewer live instances after cleanup is stronger evidence than a claim that memory “looks lower.”