Audience: Majority iOS Engineers
Written for engineers who debug systems not syntax.
Context: Large, modular, long-lived production apps
The Comfortable Lie We All Learned
(Why [weak self] Is Not the Fix You Think It Is (UIKit + SwiftUI))
For years, most of us, iOS developers have been told:
“If you have a memory leak, just add
[weak self].”
It sounds reasonable.
It sounds technical.
And it’s fundamentally incomplete.
In real-world iOS applications especially those with multiple teams, modules, coordinators, and long-lived services, memory leaks are rarely caused by missing weak keywords.
They are caused by architecture.
The Core Truth
Memory leaks are not language problems.
They are ownership problems.
Swift is memory-safe. ARC works.
What fails is our understanding of who owns what and for how long.
Once an app grows beyond a few screens, memory management stops being about syntax and becomes a systems design concern.
The [weak self] Myth
Most teams follow this pattern:
- App starts leaking
- Instruments shows retained objects
- Someone adds
[weak self] - Leak “goes away”
- Six weeks (or may be 2-3 sprints) later, it’s back!!!
Why?
Because [weak self] does not define ownership.
It only avoids one specific retain cycle.
[weak self]hides the symptom, it does not fix the cause.
If your architecture is wrong, the leak will resurface elsewhere.
Real-World Leak #1: ViewModels Without a Lifetime / ViewModel as a God Object
The most common production leak.
How it starts
A ViewModel is created to:
- Hold state
- Handle user actions
How it grows
Soon it owns:
- Network calls
- Navigation
- Analytics
- Feature flags
- Deep links
Eventually you get:
ViewController → ViewModel → Services → Closures → ViewController
Root cause:
The ViewModel has no clearly defined lifetime.
Should it:
- Should it die with the screen?
- Should it live across navigation?
- Be shared across flows?
When the answer is unclear, ARC cannot help you.
Learning: When an object owns “everything”, nothing can release it safely.
Real-World Leak #2: Long-Lived Objects Owning Short-Lived Ones
This is subtle and extremely destructive.
Common examples
-
AppCoordinatorretaining screen callbacks - Singleton managers holding ViewModels
- Global caches storing closures/callbacks
AnalyticsManager.shared.track {
viewModel.handleResult()
}
Even if viewModel is weak, the manager’s lifetime outlives the screen.
Rule:
A long-lived object must never own a short-lived one.
If it does, leaks are guaranteed.
Learning: A long-lived object must never own a short-lived one, directly or indirectly.
Combine: When Ownership Is Ignored
I would say, Combine didn’t introduce leaks.
It exposed architectural mistakes(or may be it just amplified bad architecture)!!!
For example, the culprit
publisher
.sink { value in
self.updateUI(value)
}
.store(in: &cancellables)
The leak depends on where cancellables lives:
- ViewController → usually safe
- ViewModel → depends on lifetime
- Singleton / Coordinator → leak
Learning: Combine pipelines must obey the same lifetime rules as views or they leak quietly.
NotificationCenter Is Not the Villain
NotificationCenter leaks are usually symptoms, not causes.
If deinit isn’t called, ask:
- Who is retaining this object?
- Why does it live longer than expected?
- ⚠️
NotificationCenteris rarely the root cause, it’s the first visible symptom of bad ownership.
Timers & DisplayLinks: Perfect Leak Multipliers
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.refresh()
}
Even with [weak self], this leaks if:
- The timer isn’t invalidated
- The run loop outlives the screen
Learning: Repeating resources must be explicitly tied to lifecycle; architecture decides that, not syntax.
SwiftUI: Memory Leaks Hidden Behind Simplicity
SwiftUI didn’t remove memory management.
It made it implicit.
That makes ownership mistakes easier and harder to detect.
SwiftUI Leak Pattern #1: @StateObject in the Wrong Place
struct ScreenView: View {
@StateObject var viewModel = ScreenViewModel()
}
If ScreenView is recreated:
- The ViewModel may survive longer than expected
- Or be recreated repeatedly
If you don’t control where an object is created,
you don’t control when it dies.
@StateObject doesn’t manage memory, it declares ownership.
And ownership must match lifecycle.
SwiftUI Leak Pattern #2: EnvironmentObjects With No Exit
EnvironmentObjects are effectively global ownership.
If they:
- Hold services
- Retain closures
- Reference UI state
They will live far longer than the UI expects.
Environment ≠ lifecycle.
SwiftUI Leak Pattern #3: View Identity Mistakes
Incorrect id usage causes:
- View recreation
- Object graph duplication
- State retention bugs
List(items, id: \.self) { item in
RowView(item: item)
}
If identity is unstable, memory behavior is unpredictable.
SwiftUI Leak Pattern #4: Navigation Retains State
Navigation stacks can:
- Retain views
- Retain ViewModels
- Retain closures
Even when UI disappears.
Navigation is ownership.
Treat it as such.
Architecture Smells That Predict Leaks
If you see these, leaks are inevitable:
- ViewModels doing navigation
- Singletons coordinating flows
- Managers with no lifecycle
- Extensions as dumping grounds
- “Convenience” APIs growing endlessly
These are not style issues.
They are runtime memory failures waiting to happen.
How "Good" Engineers Debug Leaks
Not by guessing!!!
Actual workflow
- Instruments → Allocations
- Memory Graph Debugger
- Identify retention chains
- Ask one question:
Should this object still be alive?
If the answer is no, the architecture is wrong.
You don’t hunt leaks.
You prove ownership is invalid.
The Architectural Fix(Not a tiplist)
Strong apps have:
- Explicit lifetimes
- Directional ownership
- Short-lived objects owned by short-lived parents
- Long-lived objects that never retain UI
This applies equally to:
- UIKit
- SwiftUI
- Combine
- Coordinators
- Dependency graphs
Final Truth
Memory leaks are not Swift problems.
They are architectural lies revealed by runtime.
Fix ownership.
Define lifetimes.
Leaks disappear naturally.






Top comments (0)