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:
- Inheritance: A class can inherit functionality from a parent class, allowing you to build hierarchies of related types.
- 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 aframe
(size and position), abackgroundColor
, and methods for handling touch events.UILabel
,UIButton
, andUIImageView
(the subclasses) inherit all ofUIView
’s properties and methods, and then add their own specialised functionality. AUILabel
adds atext
property, and aUIButton
adds the ability to have atitle
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.