DEV Community

Vinayak G Hejib
Vinayak G Hejib

Posted on

Memory Leaks Are Architecture Problems

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:

  1. App starts leaking
  2. Instruments shows retained objects
  3. Someone adds [weak self]
  4. Leak “goes away”
  5. 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
Enter fullscreen mode Exit fullscreen mode

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

  • AppCoordinator retaining screen callbacks
  • Singleton managers holding ViewModels
  • Global caches storing closures/callbacks
AnalyticsManager.shared.track {
    viewModel.handleResult()
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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?
  • ⚠️ NotificationCenter is 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()
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Instruments → Allocations
  2. Memory Graph Debugger
  3. Identify retention chains
  4. 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)