Memory leaks are sneaky. Your app works fine during testing, ships to the App Store, and then you start getting reports of random crashes. The culprit? Objects that never get deallocated, quietly eating up memory until iOS says “enough” and kills your app.
I’ve spent more hours than I’d like to admit hunting down retain cycles. Here’s what I’ve learned about using Xcode’s Memory Graph Debugger to track them down.
What’s Actually Happening
In Swift and Objective-C, ARC (Automatic Reference Counting) handles memory management for you—most of the time. The problem starts when two objects hold strong references to each other. Neither can be deallocated because each is keeping the other alive. Their retain counts never hit zero, deinit never gets called, and you’ve got a leak.
The classic example is a view controller that holds a closure which captures self. If that closure is stored somewhere long-lived (like a notification observer), you’ve got a cycle.
Why You Should Care
Beyond the obvious “my app crashes” problem, memory leaks cause weird bugs. Imagine a leaked observer still receiving notifications for a view controller that should be gone. Suddenly you’re updating UI that doesn’t exist, or worse, executing business logic that shouldn’t run.
The Memory Graph Debugger
Xcode has a built-in tool that’s genuinely useful here. While your app is running, click the three-node icon in the debug bar—it’s between the visual debugger and location simulator buttons.

This captures a snapshot of everything currently in memory. The left panel shows all resident objects and their instance counts. Select an object, and Xcode draws the reference chain keeping it alive. Bold lines mean strong references.

How I Actually Use It
My workflow goes something like this:
- Run the app and exercise a feature—open a screen, do some stuff, close it
- Repeat a few times
- Capture a memory snapshot
- Look for objects that shouldn’t be there
If I opened and closed a settings screen three times and see three SettingsViewController instances in memory, something’s wrong. There should be zero.
When you find a suspect, trace the reference chain backwards. The graph shows you exactly what’s keeping the object alive. Usually it’s a closure that needs [weak self], or a delegate property that should be weak.
Preventing Retain Cycles
A few rules of thumb that have saved me grief:
- Closures that outlive the current scope: Use
[weak self]or[unowned self] - Delegate properties: Almost always should be
weak - Notification observers: Remove them in
deinit(or use the block-based API that handles cleanup) - Timer references: Invalidate timers and use weak references
The Memory Graph Debugger won’t catch everything automatically, but it gives you the visibility to find problems yourself. It’s one of those tools I wish I’d learned earlier.