← Back to list

iOS Swift Performance Master Series — Part 2: Why Doesn’t deinit Get Called?

iOS Why Doesn’t deinit Get Called?

Abdulkadir Oruç in iCommunity · 2026-02-07 15:13 · 150 claps · 3.4 min read
#swift #swift-programming #swift-performance #deinit #weak-self
Open on Medium ↗
Wiki topics: 💻 · Programming 📱 · Mobile Development

Why Doesn’t deinitGet Called? Comprehensive Scenarios: iOS Swift Performance Master Series Part 2

1. What Is deinit?

deinit is a special method that is called right before a class instance is completely released from memory.

class Example {
    deinit {
        print("Example deinit")
    }
}
  • deinit cannot be called manually.
  • It is automatically triggered by ARC.
  • If deinit is called, it means the object’s retain count has reached 0.
  • If deinit is not called, this is not a bug; it is proof that ARC still thinks the object is needed.

2. Why Doesn’t deinit Run?

There is one single, immutable reason:

There is still at least one strong reference to the object.

All scenarios revolve around this fact.

deinit is not a debug print and not a “did it work or not” indicator.

If deinit does not run:

  • The app holds unnecessary memory
  • Performance degrades
  • Scrolling, animations, and networking slow down

3. Most Common Scenarios

3.1 Retain Cycle (Strong Reference Cycle)

A retain cycle occurs when two or more objects hold each other with strong references, preventing ARC from releasing any of them.

class A {
    var b: B?
}
class B {
    var a: A?
}
  • A holds B
  • B holds A
  • Retain count never reaches 0
  • deinit is never called

Solution: the non-owning side must be weak or unowned.

class B {
    weak var a: A?
}

3.2 ViewController — ViewModel Relationship (MVVM)

Problem:

class ViewModel {
    var viewController: MyVC
}
  • ViewController owns ViewModel.
  • ViewModel strongly owns ViewController.
  • Even after UI dismissal, both remain in memory.

Correct model:

class ViewModel {
    weak var viewController: MyVC?
}

The ViewModel does not own the ViewController.

3.3 Closures

Closures capture variables from their defining scope. Class instances are captured strongly by default.

Problematic example:

class MyVC {
    func load() {
        service.fetch {
            self.updateUI()
        }
    }
}

Reference chain:

  • ViewController → service
  • service → closure
  • closure → ViewController

Solution using a capture list:

service.fetch { [weak self] in
    self?.updateUI()
}

3.4 Timer

A Timer:

  • Does not stop automatically
  • Is added to the RunLoop
  • Fires repeatedly until invalidated
  • Holds its target strongly
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    self.tick()
}

As long as the timer runs, the ViewController cannot be released.

Solution:

timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
    self?.tick()
}

Now:

  • The screen can be dismissed
  • self becomes nil
  • No crash
  • No memory leak

However, the timer is still running in the background and firing every second. The closure exits immediately, but CPU and energy are still consumed.

One timer is negligible. Many timers across many screens accumulate into a real performance issue.

To fully stop it:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    timer?.invalidate()
    timer = nil
}

After invalidate():

  • The timer is removed from the RunLoop
  • It never fires again

3.5 Singletons

Singletons live for the entire lifetime of the app.

class Manager {
    static let shared = Manager()
}

If a singleton strongly holds a ViewController:

class Manager {
    static let shared = Manager()
    var delegate: SomeViewController?
}

And somewhere:

Manager.shared.delegate = self

Result:

  • Singleton never deallocates
  • ViewController never deallocates
  • deinit is never called
  • Memory leak occurs

Singletons should not own UI objects.

If a reference is required:

class Manager {
    static let shared = Manager()
    weak var delegate: SomeViewController?
}

Now:

  • ViewController can be released
  • Singleton remains alive
  • No leak

3.6 Combine / RxSwift

Publisher and Subscriber are connected via a Subscription. As long as the subscription is active, the chain remains.

publisher
    .sink { value in
        self.handle(value)
    }
  • Publisher holds Subscriber
  • Subscriber holds ViewController
  • Subscription never completes
  • ViewController cannot be released

Correct approach:

publisher
    .sink { [weak self] value in
        self?.handle(value)
    }
    .store(in: &cancellables)

When the ViewController deinitializes:

  • cancellables deinitializes
  • All subscriptions are cancelled automatically

3.7 Async / Await & Task

Task {
    await self.load()
}

Task strongly captures self.

Solution:

Task { [weak self] in
    await self?.load()
}

or explicitly cancel the task:

task.cancel()

4. Golden Rules

4.1 Ask “Who Owns This?”

Ownership means controlling an object’s lifetime.

ARC perspective:

  • Owner holds a strong reference
  • Non-owner holds weak or unowned

Incorrect ownership:

class ViewModel {
    var viewController: MyViewController
}

Correct ownership:

class ViewModel {
    weak var viewController: MyViewController?
}

Golden rule: The UI layer is usually owned, not the owner.

4.2 Closure Reflex: [weak self]

You never know how long a closure will live:

  • Network delays
  • Long async operations
  • Suspended tasks

Default reflex:

service.fetch { [weak self] in
    self?.updateUI()
}

4.3 If You Don’t See deinit

If deinit doesn’t run:

  • Don’t guess
  • Don’t speculate
  • Don’t remove code randomly

Use Memory Graph Debugger.

Ask:

  • Who is holding this object?
  • Is the reference strong?
  • Is it necessary?

If deinit doesn’t fire, the problem is not where you’re looking — it’s where the object is being retained.

With this section, we’ve covered the most fundamental and critical topics for iOS performance optimization and learned how to prevent the core scenarios. In the next parts of the series, we’ll move to higher-level concepts and complete the full picture. 🚀


메타데이터
post_id
619e2acbb189
slug
ios-swift-performance-master-series-part-2-why-doesnt-deinit-get-called-619e2acbb189
url
https://medium.com/icommunity/ios-swift-performance-master-series-part-2-why-doesnt-deinit-get-called-619e2acbb189
canonical_url
https://medium.com/icommunity/ios-swift-performance-master-series-part-2-why-doesnt-deinit-get-called-619e2acbb189
author_url
https://medium.com/@kadiroruc
status
ok
fetched_at
2026-06-10 21:21:38