Lesson 5 • Intermediate
Object-Oriented Programming 🏗️
By the end of this lesson you'll know exactly when to use a struct vs a class — Swift's most important modelling decision — and you'll build your own types with properties, methods, inheritance, and protocols, the foundation of every Swift app.
What You'll Learn
- Tell value types (struct) from reference types (class) — and predict copy vs share behaviour
- Add stored and computed properties, plus a didSet property observer
- Write methods, and use mutating to change a struct from inside
- Define initialisers (init) that set up a new instance
- Use class inheritance and override a method with super
- Write protocols (Swift's interfaces) and apply protocol-oriented programming
1️⃣ Structs vs Classes (value vs reference)
This is the single most important idea in Swift. A struct is a value type: when you assign it to another variable, Swift makes a full, independent copy. A class is a reference type: assigning it just hands out another pointer to the same object. Get this right and most Swift "why did that change?!" bugs disappear. Read the worked example, run it, and watch how the copy and the shared object behave differently.
// A STRUCT is a VALUE type. Assigning it makes an independent COPY.
struct Point {
var x: Int
var y: Int
}
var pointA = Point(x: 3, y: 5)
var pointB = pointA // pointB is a separate COPY of pointA
pointB.x = 99 // change the copy only
print("pointA = (\(pointA.x), \(pointA.y))") // (3, 5) — untouched
print("pointB = (\(pointB.x), \(pointB.y))") // (99, 5) — copy changed
// A CLASS is a REFERENCE type. Assigning it SHARES the same object.
class Counter {
var value: Int
init(value: Int) { // an initialiser sets up a new instance
self.value = value // self.value = the property; value = the argument
}
}
let counterA = Counter(value: 0)
let counterB = counterA // counterB points to the SAME object as counterA
counterB.value = 99 // there is only one object, so both "see" it
print("counterA.value = \(counterA.value)") // 99 — same object!
print("counterB.value = \(counterB.value)") // 99pointA = (3, 5)
pointB = (99, 5)
counterA.value = 99
counterB.value = 992️⃣ Stored, Computed & Observed Properties
A stored property keeps a value in memory. A computed property stores nothing — it runs a little code to work out its value every time you read it, which keeps related values in sync for free. A property observer like didSet runs automatically whenever a stored property changes, handing you the previous value as oldValue.
struct Temperature {
// STORED property — holds a value in memory.
var celsius: Double {
// PROPERTY OBSERVER — didSet runs every time celsius changes.
didSet {
print("Temp changed: \(oldValue)°C -> \(celsius)°C")
}
}
// COMPUTED property — no storage; it calculates on every read.
var fahrenheit: Double {
celsius * 9 / 5 + 32
}
}
var temp = Temperature(celsius: 20)
print("Start: \(temp.celsius)°C = \(temp.fahrenheit)°F") // 20.0 = 68.0
temp.celsius = 100 // didSet fires here
print("Now: \(temp.celsius)°C = \(temp.fahrenheit)°F") // 100.0 = 212.0Start: 20.0°C = 68.0°F
Temp changed: 20.0°C -> 100.0°C
Now: 100.0°C = 212.0°F🎯 Your Turn: mutate a struct
A method that changes its own struct's properties needs one special keyword. Fill in the two ___ blanks using the // 👉 hints, then run it and match the expected output.
// 🎯 YOUR TURN — fill in the blanks marked with ___
struct Player {
let name: String
var score: Int
// A method that CHANGES a struct's own property must be marked...
___ func addPoints(_ points: Int) { // 👉 the keyword is: mutating
// Add "points" to the current score
score ___ points // 👉 use += to add into score
print("\(name) scored! Total: \(score)")
}
}
var player = Player(name: "Sam", score: 0)
player.addPoints(10)
player.addPoints(5)
// ✅ Expected output:
// Sam scored! Total: 10
// Sam scored! Total: 15Sam scored! Total: 10
Sam scored! Total: 15___ using the // 👉 hints, then run it at SwiftFiddle. The Output panel shows what you should get.3️⃣ Class Inheritance & Overriding
Only classes support inheritance. A subclass written as class Dog: Animal gets everything the parent has, and can replace any method by marking it override (the keyword is required, so you never override something by accident). Inside a subclass initialiser you call super.init(...) to let the parent set up its own properties first.
// A base class with stored properties and methods.
class Animal {
let name: String
init(name: String) { // required initialiser — every Animal has a name
self.name = name
}
func speak() -> String {
"\(name) makes a sound"
}
}
// Dog INHERITS from Animal (single inheritance with a colon).
class Dog: Animal {
let breed: String
init(name: String, breed: String) {
self.breed = breed
super.init(name: name) // call the parent's initialiser
}
// OVERRIDE replaces the inherited method. The keyword is required.
override func speak() -> String {
"\(name) the \(breed) says Woof!"
}
}
let generic = Animal(name: "Creature")
let rex = Dog(name: "Rex", breed: "Corgi")
print(generic.speak()) // Creature makes a sound
print(rex.speak()) // Rex the Corgi says Woof! (overridden)
print("Rex is still an Animal: \(rex is Animal)") // trueCreature makes a sound
Rex the Corgi says Woof!
Rex is still an Animal: true4️⃣ Protocols & Protocol-Oriented Programming
A protocol is Swift's version of an interface: a contract listing the properties and methods a type must provide, with no implementation. Structs, classes, and enums can all conform to it. The superpower is the protocol extension — it supplies a default implementation every conformer gets for free. Writing code against protocols rather than concrete types is protocol-oriented programming, and it's the heart of idiomatic Swift.
// A PROTOCOL is a contract: WHAT a type must do, not HOW.
protocol Shape {
var name: String { get } // conformers must provide a name
func area() -> Double // ...and an area() method
}
// A protocol EXTENSION gives every conformer a default method for free.
extension Shape {
func describe() -> String {
"\(name) has area \(area())"
}
}
// A STRUCT can conform to a protocol (so can a class or enum).
struct Square: Shape {
let side: Double
let name = "Square"
func area() -> Double { side * side }
}
struct Circle: Shape {
let radius: Double
let name = "Circle"
func area() -> Double { (radius * radius * 3.14 * 100).rounded() / 100 }
}
// Protocol-oriented programming: code to the protocol, not the type.
let shapes: [Shape] = [Square(side: 4), Circle(radius: 2)]
for shape in shapes {
print(shape.describe()) // uses the default describe()
}Square has area 16.0
Circle has area 12.56🎯 Your Turn: conform to a protocol
Make the Robot struct adopt the Greetable protocol and implement the method it requires. Fill in the two blanks, then run it.
// 🎯 YOUR TURN — make Robot conform to the Greetable protocol.
protocol Greetable {
var name: String { get }
func greet() -> String
}
// Make Robot adopt the Greetable protocol (add it after the colon).
struct Robot: ___ { // 👉 the protocol name is: Greetable
let name: String
// Provide the method the protocol requires.
func greet() -> String {
"Beep boop, I am \(___)" // 👉 use the stored property: name
}
}
let r2 = Robot(name: "R2-D2")
print(r2.greet())
// ✅ Expected output:
// Beep boop, I am R2-D2Beep boop, I am R2-D2___ using the // 👉 hints, then run it at SwiftFiddle and check the Output panel.Pro Tips
- 💡 Default to a struct. Only switch to a class when you genuinely need shared state, inheritance, or a deinitialiser.
- 💡 Computed over duplicated. If one value can always be derived from another (Fahrenheit from Celsius), make it computed so the two can never drift out of sync.
- 💡 Protocol extensions beat base classes for sharing behaviour across value types — they avoid fragile, deep class hierarchies.
- 💡 Mark classes
finalwhen they aren't designed to be subclassed — it documents intent and lets the compiler optimise.
Common Errors (and the fix)
- "Cannot assign to property: 'self' is immutable": a struct method is changing its own property without permission. Mark the method
mutating func .... - The value-copy surprise: you changed a struct copy and expected the original to update. Structs copy on assignment — if you truly need shared state, use a
classinstead. - "Class 'Dog' has no initializers" / "Property not initialized at super.init call": a stored property has no value. Give every non-optional property a value, and set the subclass's own properties before calling
super.init(...). - "Type 'Robot' does not conform to protocol 'Greetable'": you listed the protocol but didn't implement everything it requires. Add the missing property or method exactly as the protocol declares it.
- "Overriding declaration requires an 'override' keyword": you redefined an inherited method. Add
overridein front offunc.
📋 Quick Reference — struct vs class
| Feature | struct (value type) | class (reference type) |
|---|---|---|
| On assignment | Copied (independent) | Shared (same object) |
| Inheritance | No | Yes (single) |
| Change own property in a method | Needs mutating | Just works |
| Free initialiser | Yes (memberwise) | No — write init |
| Override a method | N/A | override func ... |
| Use it when… | Default choice; simple data | Shared state / inheritance |
Frequently Asked Questions
Q: When should I use a struct vs a class in Swift?
Reach for a struct by default — it is a value type, so copies are independent, which makes code safer and easier to reason about. Use a class only when you need reference semantics (several variables sharing one object), inheritance, or deinitialisers. Apple's own guidance and SwiftUI are struct-first.
Q: What is the difference between a stored and a computed property?
A stored property holds a value in memory (var celsius: Double). A computed property has no storage of its own — it runs code to calculate a value every time you read it (var fahrenheit: Double { celsius * 9 / 5 + 32 }). Computed properties keep related values in sync automatically.
Q: Why do I need the 'mutating' keyword on struct methods?
Structs are value types, and their methods cannot change the instance's own properties unless you mark the method 'mutating'. This is Swift making the cost of mutation explicit. Classes do not need it because they are reference types. If you forget it, you get the error 'cannot assign to property: self is immutable'.
Q: Are Swift protocols the same as interfaces in Java or C#?
They are close — both define a contract of methods and properties a type must provide. Swift goes further: structs, classes, and enums can all conform; a type can conform to many protocols; and protocol extensions can supply default implementations. That combination is what enables protocol-oriented programming.
Q: Can a struct inherit from another struct?
No. Only classes support inheritance in Swift. Structs and enums cannot subclass. To share behaviour across value types, use a protocol with a protocol extension instead — that is the idiomatic Swift approach and avoids the fragility of deep class hierarchies.
Mini-Challenge: a Vehicle protocol
No blanks this time — just a brief and an outline. Build it yourself, run it, and check your output against the example in the comments. This is exactly how real Swift models a family of related types.
// 🎯 MINI-CHALLENGE: a Vehicle protocol with two conformers
//
// 1. Define a protocol "Vehicle" requiring:
// - a stored property wheels: Int { get }
// - a method describe() -> String
// 2. Make a struct "Car" (wheels = 4) and a struct "Motorcycle" (wheels = 2),
// each conforming to Vehicle, each printing its own description.
// 3. Put both in an array of type [Vehicle] and loop, calling describe().
//
// ✅ Example output:
// A car with 4 wheels
// A motorcycle with 2 wheels
// your code here🎉 Lesson Complete!
- ✅ Structs are value types (copied) and classes are reference types (shared) — the key Swift distinction
- ✅ Properties come in three flavours: stored, computed, and observed with
didSet - ✅ Changing a struct from inside a method needs
mutating; aninitsets up a new instance - ✅ Classes support inheritance; replace a method with
overrideand callsuper.init - ✅ Protocols are Swift's interfaces; protocol extensions add defaults — the basis of protocol-oriented programming
- ✅ Next lesson: SwiftUI Basics — use these types to build real user interfaces
Sign up for free to track which lessons you've completed and get learning reminders.