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.

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:

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 = nil

When 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 = nil

The 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 = nil

Here 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 = nil

Because 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.

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 = nil

Fix: 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 = nil

The 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 = nil

The 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

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 = nil

This 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

11. Practice Exercise

Try building a class that tracks a temporary editing session.

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 = nil

12. 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.