iOS Swift Performance Master Series — Part 2: Why Doesn’t deinit Get Called?
iOS Why Doesn’t deinit Get Called?
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")
}
}
deinitcannot be called manually.- It is automatically triggered by ARC.
- If
deinitis called, it means the object’s retain count has reached 0. - If
deinitis 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
deinitis 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
selfbecomes 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
deinitis 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:
cancellablesdeinitializes- 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