Swift Deinitializers: How deinit Works and When It Runs
Swift deinitializers let a class instance clean up just before it is removed from memory. If you are learning classes and memory management, understanding deinit helps you write safer code, release resources at the right time, and diagnose bugs such as objects that never seem to go away.
Quick answer: A Swift deinitializer is the deinit block inside a class. It runs automatically when the last strong reference to that class instance is removed, and it is mainly used to perform final cleanup such as stopping observers, closing resources, or logging object destruction.
Difficulty: Beginner
Helpful to know first: You'll understand this better if you know basic Swift class syntax, how properties work, and that Swift uses Automatic Reference Counting (ARC) to manage class instances.
1. What Is a Deinitializer?
A deinitializer is special code that belongs to a class and runs automatically when the instance is about to be removed from memory.
- It is written with the keyword deinit.
- Only classes can have deinitializers.
- You do not call a deinitializer yourself.
- It takes no parameters and has no return value.
- It is commonly used for final cleanup work.
In Swift, class instances are reference types. That means multiple variables can point to the same object. Swift uses ARC to keep track of how many strong references exist. When that count reaches zero, Swift destroys the instance and runs its deinitializer.
A deinitializer is closely related to an initializer, but they are not opposites in every detail. An initializer sets an object up when it is created. A deinitializer tears it down when it is no longer needed. Unlike init, a class can have only one deinit.
2. Why Deinitializers Matter
Many class instances do not need a custom deinitializer at all, because ARC automatically frees ordinary stored properties. However, some objects manage work or resources that should be stopped or released explicitly.
Deinitializers matter because they help you:
- remove observers before the object disappears
- invalidate timers or ongoing tasks
- close files or network-related helpers owned by the object
- release custom unmanaged resources
- debug object lifetime by printing when an instance is destroyed
Without proper cleanup, an object may leave behind extra work, unexpected behavior, or hidden memory problems. For example, if a class starts a timer and never invalidates it, the timer may continue running longer than expected.
A deinitializer is for cleanup, not for normal business logic. If your program depends on deinit to perform everyday application work, the design is usually too fragile.
3. Basic Syntax or Core Idea
Simple deinitializer syntax
The basic syntax is short. You place a deinit block inside a class definition.
class FileSession {
let name: String
init(name: String) {
self.name = name
print("Opened \(name)")
}
deinit {
print("Closed \(name)")
}
}This class prints a message when it is created and another message when it is destroyed. The deinitializer has no parentheses because it cannot accept arguments.
When it runs
Here is a minimal example showing object lifetime.
class UserSession {
let username: String
init(username: String) {
self.username = username
print("Session started for \(username)")
}
deinit {
print("Session ended for \(username)")
}
}
var session: UserSession? = UserSession(username: "Taylor")
session = nilWhen session is set to nil, the last strong reference is removed, so the object is deallocated and deinit runs.
deinit vs init
The two are related but used differently.
class Counter {
var value = 0
init() {
print("Counter created")
}
deinit {
print("Counter destroyed")
}
}init prepares the object for use. deinit performs final cleanup after the object is no longer needed.
4. Step-by-Step Examples
Example 1: Watching a simple object lifecycle
This first example shows the easiest way to see a deinitializer in action.
class Message {
let text: String
init(text: String) {
self.text = text
print("Created: \(text)")
}
deinit {
print("Destroyed: \(text)")
}
}
var message: Message? = Message(text: "Hello")
message = nilThe output shows creation first and destruction second. This helps beginners understand that deinit is tied to the instance lifecycle.
Example 2: Multiple references to the same object
A deinitializer does not run until the last strong reference is gone.
class Document {
let title: String
init(title: String) {
self.title = title
}
deinit {
print("Document removed: \(title)")
}
}
var doc1: Document? = Document(title: "Report")
var doc2 = doc1
doc1 = nil
print("doc1 cleared")
doc2 = nil
print("doc2 cleared")After doc1 = nil, the instance still exists because doc2 still references it. Only after doc2 = nil does the deinitializer run.
Example 3: Cleaning up helper work
This example simulates an object that starts work and must stop it before being destroyed.
class DownloadTask {
var isRunning = false
init() {
isRunning = true
print("Download started")
}
func stop() {
if isRunning {
isRunning = false
print("Download stopped")
}
}
deinit {
stop()
print("DownloadTask destroyed")
}
}
var task: DownloadTask? = DownloadTask()
task = nilHere the deinitializer acts as a final safety net. If the task is still running when the object disappears, cleanup still happens.
Example 4: Breaking a strong reference chain with weak
One common reason a deinitializer does not run is a retain cycle. This example shows the correct use of a weak reference.
class Owner {
let name: String
var device: Device?
init(name: String) {
self.name = name
}
deinit {
print("Owner destroyed: \(name)")
}
}
class Device {
let model: String
weak var owner: Owner?
init(model: String) {
self.model = model
}
deinit {
print("Device destroyed: \(model)")
}
}
var owner: Owner? = Owner(name: "Chris")
var device: Device? = Device(model: "Tablet")
owner?.device = device
device?.owner = owner
owner = nil
device = nilBecause owner inside Device is weak, both objects can be released normally. If both references were strong, neither object would deinitialize.
5. Practical Use Cases
Deinitializers are most useful in classes that own something needing explicit shutdown or cleanup.
- Stopping a custom worker object when a screen controller or manager object goes away
- Removing notifications or observation tokens owned by a class instance
- Invalidating a timer that should not continue after the object is gone
- Closing a file handle wrapper or similar resource-owning helper
- Logging object destruction while debugging memory leaks
- Releasing unmanaged or low-level resources wrapped by a Swift class
If a class only stores ordinary Swift values and references that ARC can manage automatically, you may not need a deinitializer at all.
6. Common Mistakes
Mistake 1: Expecting deinit to exist in a struct
Deinitializers belong to classes, not structures or enumerations. Beginners sometimes try to use deinit with a value type because they want cleanup behavior everywhere.
Problem: This code uses deinit in a struct, which Swift does not allow because structs are value types and are not managed by ARC in the same way as classes.
struct FileRecord {
let name: String
deinit {
print("Cleaning up \(name)")
}
}Fix: Use a class if you need object lifetime behavior tied to ARC and deinit.
class FileRecord {
let name: String
init(name: String) {
self.name = name
}
deinit {
print("Cleaning up \(name)")
}
}The corrected version works because classes are reference types and support deinitializers.
Mistake 2: Assuming deinit runs as soon as one variable becomes nil
If an object has multiple strong references, clearing only one of them is not enough. The instance stays alive until the last strong reference disappears.
Problem: This code assumes setting one variable to nil destroys the object immediately, but another strong reference still keeps it alive.
class Account {
deinit {
print("Account destroyed")
}
}
var firstRef: Account? = Account()
var secondRef = firstRef
firstRef = nilFix: Remove every strong reference before expecting the object to deinitialize.
class Account {
deinit {
print("Account destroyed")
}
}
var firstRef: Account? = Account()
var secondRef = firstRef
firstRef = nil
secondRef = nilThe corrected version works because ARC can finally reduce the strong reference count to zero.
Mistake 3: Creating a retain cycle so deinit never runs
Two class instances can keep each other alive if both store strong references. This is one of the most common reasons developers think deinit is "not working".
Problem: Both objects strongly reference each other, so ARC cannot reduce either one to zero references. Their deinitializers never run.
class Customer {
var card: CreditCard?
deinit {
print("Customer destroyed")
}
}
class CreditCard {
var customer: Customer?
deinit {
print("CreditCard destroyed")
}
}Fix: Make one side weak or unowned, depending on the ownership relationship.
class Customer {
var card: CreditCard?
deinit {
print("Customer destroyed")
}
}
class CreditCard {
weak var customer: Customer?
deinit {
print("CreditCard destroyed")
}
}The corrected version works because the weak reference does not increase the strong reference count.
Mistake 4: Trying to call deinit manually
A deinitializer is controlled by ARC, not by your own code. You cannot invoke it like a normal method.
Problem: This code treats deinit like a function, but Swift does not allow manual calls to a deinitializer.
class Logger {
deinit {
print("Logger destroyed")
}
}
let logger = Logger()
// logger.deinit()Fix: Remove the strong references instead, or create a separate cleanup method if you need explicit manual shutdown before deallocation.
class Logger {
func close() {
print("Logger closed manually")
}
deinit {
print("Logger destroyed")
}
}
var logger: Logger? = Logger()
logger?.close()
logger = nilThe corrected version works because manual cleanup and automatic deallocation are handled separately.
7. Best Practices
Use deinit for cleanup, not primary program flow
It is fine to release resources or stop work in a deinitializer, but your app should not depend on deinit to trigger essential user-facing behavior at a precise moment.
class Uploader {
func finishUpload() {
print("Upload finished")
}
deinit {
print("Uploader destroyed")
}
}The preferred pattern is to use a normal method for explicit business actions and reserve deinit for final cleanup.
Keep deinitializers short and predictable
A deinitializer should usually perform quick cleanup tasks. Very complex work inside deinit can make object lifetimes harder to reason about.
class ObserverBox {
var isObserving = true
func stopObserving() {
if isObserving {
isObserving = false
print("Observation removed")
}
}
deinit {
stopObserving()
}
}This is easier to understand than packing a lot of unrelated logic directly into the deinitializer.
Use weak or unowned references deliberately
If two objects reference each other, think carefully about ownership. One side often should not strongly own the other.
class Department {
var manager: Manager?
}
class Manager {
weak var department: Department?
}This practice prevents retain cycles and allows deinitializers to run when the objects are no longer needed.
8. Limitations and Edge Cases
- Only classes can define deinit. Structs and enums cannot.
- A class can have only one deinitializer.
- You cannot call deinit directly.
- The exact moment of deinitialization depends on ARC and remaining strong references.
- If a retain cycle exists, deinit may never run until the cycle is broken.
- Stored properties are released automatically, so you do not need deinitializers for ordinary memory cleanup alone.
- If a class inherits from another class, the subclass deinitializer runs before the superclass is deinitialized as the instance is torn down.
- A common "deinit not working" situation is actually a hidden strong reference, such as a closure or another object still holding the instance.
When debugging lifecycle issues, adding a simple print inside deinit is a practical way to see whether an object is actually being released.
9. Practical Mini Project
This mini project creates a small class that simulates a connection. It starts active, can be closed manually, and also guarantees cleanup in its deinitializer.
class Connection {
let id: Int
private var isOpen = false
init(id: Int) {
self.id = id
isOpen = true
print("Connection \(id) opened")
}
func send(message: String) {
if isOpen {
print("Sending: \(message)")
} else {
print("Cannot send, connection is closed")
}
}
func close() {
if isOpen {
isOpen = false
print("Connection \(id) closed manually")
}
}
deinit {
if isOpen {
print("Connection \(id) closed in deinit")
}
print("Connection \(id) destroyed")
}
}
var connection: Connection? = Connection(id: 101)
connection?.send(message: "Hello server")
connection?.close()
connection = nilThis example shows a clean design: explicit manual closing is available through close(), and deinit acts as a backup cleanup point when the object is finally released.
10. Key Points
- deinit is a class-only feature used for final cleanup before an instance is removed from memory.
- Swift runs a deinitializer automatically when the last strong reference to the instance is gone.
- You cannot call a deinitializer yourself, and it does not take parameters.
- Most ordinary memory cleanup is handled by ARC automatically.
- Deinitializers are especially useful for stopping work, removing observers, or releasing owned resources.
- If deinit does not run, a hidden strong reference or retain cycle is often the cause.
- weak and unowned references help prevent cycles that block deinitialization.
11. Practice Exercise
Try building a class that tracks a temporary editing session.
- Create a class named EditingSession with a documentName property.
- Print a message when the session starts.
- Add a method named endSession() that prints a manual closing message.
- Add a deinit that prints a destruction message.
- Create an optional instance, call endSession(), then set the variable to nil.
Expected output: You should see a start message, then a manual end message, then a deinitialization message.
Hint: Use a class, not a struct, and make the variable optional so you can set it to nil.
class EditingSession {
let documentName: String
init(documentName: String) {
self.documentName = documentName
print("Started editing \(documentName)")
}
func endSession() {
print("Ended session for \(documentName)")
}
deinit {
print("EditingSession destroyed for \(documentName)")
}
}
var session: EditingSession? = EditingSession(documentName: "Notes.txt")
session?.endSession()
session = nil12. Final Summary
Swift deinitializers are a small feature, but they teach an important part of how classes behave. A deinit block runs automatically when the last strong reference to a class instance disappears, which makes it the right place for final cleanup work. In everyday Swift code, that often means stopping background work, removing observers, invalidating timers, or logging object destruction while debugging.
The most important practical idea is that deinitializers depend on ARC. If an object still has a strong reference somewhere, it will not be destroyed yet. If a retain cycle exists, deinit may never run at all. Once you understand that relationship between deinit, strong references, and weak references, you can reason much more confidently about class lifetimes in Swift.
A good next step is to study Swift Automatic Reference Counting in more depth, especially strong, weak, and unowned references. That will make deinitializers feel much more predictable in real projects.