Classes are a fundamental concept in Object-Oriented Programming (OOP) and a cornerstone of Swift. In OOP, we model complex systems by creating “objects”—self-contained units that bundle data (properties) and behavior (methods). A class is the blueprint for creating these objects.

Unlike structures, classes have two key characteristics:

  1. Inheritance: A class can inherit functionality from a parent class, allowing you to build hierarchies of related types.
  2. Reference Semantics: Classes are reference types. When you pass a class instance around, you’re passing a reference to the same underlying object in memory.

Let’s look at a basic class:

class Vehicle {
    var speed: Int = 0
    var description: String {
        "Traveling at \(speed) mph"
    }
    
    func accelerate() {
        speed += 10
    }
}

Creating and Using Classes

Create instances using the class name followed by parentheses:

let car = Vehicle()
car.accelerate()
print(car.description) // Prints "Traveling at 10 mph"

Reference Type Behavior

Classes exhibit reference semantics. When you assign a class instance to a new variable, both variables point to the same object. This means that if you can change a value on one of the objects, both variables will see the change:

let car1 = Vehicle()
let car2 = car1  // car2 references the same object as car1

car1.accelerate()
print(car2.speed) // Prints 10 - both variables reference the same object

This is different from structures, which are copied when assigned.

Here’s a visual comparison:

Structs (Value Types) Create Copies

When you assign a struct, you create a completely separate copy. Changes to one don’t affect the other.

// Assuming a struct like: struct Point { var x: Int }
var a = Point(x: 10)
var b = a // b is a new copy of a
b.x = 20

print(a.x) // Prints 10
print(b.x) // Prints 20

/*
Visualized:
a -------> { x: 10 }
b -------> { x: 20 }  // a and b are separate copies
*/

Classes (Reference Types) Share Instances

When you assign a class instance, you’re just passing around a reference (or a “pointer”) to the same object in memory.

// Using the Vehicle class from before
var c = Vehicle()
var d = c // c and d both point to the same object

d.speed = 30

print(c.speed) // Prints 30

/*
Visualized:
c,d -----> { speed: 30 }  // c and d both point to the same object
*/

Inheritance

Inheritance allows a class to inherit properties and methods from another class. The inheriting class is called a subclass, and the class being inherited from is the superclass.

class Car: Vehicle {
    var gear: Int = 1
    
    override func accelerate() {
        speed += 15  // Cars accelerate faster than generic vehicles
        print("Car is now going \(speed) mph in gear \(gear)")
    }
    
    func shiftGear(to newGear: Int) {
        gear = newGear
        print("Shifted to gear \(gear)")
    }
}

The Car class inherits from Vehicle, gaining access to the speed property and accelerate() method. It also adds its own gear property and shiftGear() method.

Real-World Example: UIView in UIKit

A classic example of inheritance is UIView from Apple’s UIKit framework. Every visual element on the screen—labels, buttons, text fields, sliders—is a subclass of UIView.

  • UIView (the superclass) defines core behaviours like having a frame (size and position), a backgroundColor, and methods for handling touch events.
  • UILabel, UIButton, and UIImageView (the subclasses) inherit all of UIView’s properties and methods, and then add their own specialised functionality. A UILabel adds a text property, and a UIButton adds the ability to have a title and a target-action mechanism.

This hierarchy allows you to treat all view objects polymorphically—you can put any UIView subclass into an array of [UIView] and manage them together.

Using Inherited Classes

let myCar = Car()
myCar.accelerate()        // Uses Car's overridden method
myCar.shiftGear(to: 2)    // Uses Car's own method
print(myCar.description)  // Uses inherited property from Vehicle

Overriding Methods and Properties

Use the override keyword to replace a superclass’s implementation:

Beware: Overriding can sometimes make code harder to debug. When you call a method on an object, you need to know which implementation is being executed. This can become complex in deep inheritance hierarchies, so use it thoughtfully.

Overriding Methods

class SportsCar: Car {
    override func accelerate() {
        speed += 25  // Sports cars accelerate even faster
        print("Sports car zooming at \(speed) mph!")
    }
}

Overriding Properties

You can override computed properties to provide custom behavior:

class ElectricCar: Car {
    var batteryLevel: Int = 100
    
    override var description: String {
        "Electric vehicle at \(speed) mph with \(batteryLevel)% battery"
    }
}

Calling Superclass Methods

Use super to call the superclass’s version of a method or property:

class HybridCar: Car {
    var isElectricMode: Bool = false
    
    override func accelerate() {
        if isElectricMode {
            speed += 5  // Slower in electric mode
        } else {
            super.accelerate()  // Use parent's acceleration
        }
    }
}

Real-World Example: super.viewDidLoad()

If you’ve done any iOS development with UIKit, you’ve seen this pattern. When you subclass UIViewController, you often override the viewDidLoad() method to set up your view.

It is crucial to call super.viewDidLoad() inside your override.

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad() // Must call super!
        
        // Now, do your custom setup
        view.backgroundColor = .blue
    }
}

Why? Because the UIViewController superclass performs its own important setup inside viewDidLoad(). If you don’t call super, you skip that essential work, and your view controller might not load or behave correctly. Calling super ensures the parent class gets to do its job before the subclass adds its own behavior.

InitialiSers in Classes

Classes can have custom initializers. If a subclass adds stored properties, it must initialiSe them before calling super.init():

class Motorcycle: Vehicle {
    let engineSize: Int
    
    init(engineSize: Int) {
        self.engineSize = engineSize  // Initialize new properties first
        super.init()                  // Then call superclass initializer
        speed = 5                     // Can modify inherited properties after super.init()
    }
    
    override var description: String {
        "Motorcycle (\(engineSize)cc) at \(speed) mph"
    }
}

let bike = Motorcycle(engineSize: 600)
print(bike.description) // Prints "Motorcycle (600cc) at 5 mph"

Final Classes and Methods

Use final to prevent inheritance or overriding:

final class RaceCar: Car {  // Cannot be subclassed
    final override func accelerate() {  // Cannot be overridden further
        speed += 50
    }
}

Class vs Struct: When to Choose Classes

Use classes when you need:

  • Inheritance - Building type hierarchies with shared behaviour
  • Reference semantics - Multiple variables should reference the same instance
  • Identity - You need to track whether two variables refer to the same object
  • Deinitialisers - Custom cleanup when objects are deallocated
class DatabaseConnection {
    let connectionId: String
    
    init(connectionId: String) {
        self.connectionId = connectionId
        print("Connected to database: \(connectionId)")
    }
    
    deinit {
        print("Disconnected from database: \(connectionId)")
    }
}

Use structures for simple data containers where you want value semantics and don’t need inheritance.

Identity vs Equality

Classes support identity checking with === and !== operators:

let car1 = Car()
let car2 = Car()
let car3 = car1

print(car1 === car2)  // false - different objects
print(car1 === car3)  // true - same object

This checks if two variables reference the exact same object instance in memory, not just whether they have the same values.


Classes provide powerful modeling capabilities through inheritance and reference semantics. They’re essential when you need to share behaviour across related types or when multiple parts of your code need to work with the same object instance.

Use inheritance thoughtfully, favour composition over deep inheritance hierarchies, and consider that Swift’s protocol-oriented programming as a possibly cleaner alternative.