Object-Oriented Programming
Reviewed & published by Brayan K
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.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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)") // 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.0🎯 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: 153️⃣ 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)") // 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()
}🎯 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-D2Pro 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 final when 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 class instead.
- "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 override in front of func.
📋 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 |
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; an init sets up a new instance
- ✅ Classes support inheritance; replace a method with override and call super.init
- ✅ Protocols are Swift's interfaces; protocol extensions add defaults — the basis of protocol-oriented programming
Practice quiz
What happens when you assign a struct to another variable?
- They share one object
- An independent copy is made
- It throws an error
- The original becomes nil
Answer: An independent copy is made. Structs are value types, so assignment makes an independent copy.
A class in Swift is a...
- Value type
- Constant
- Reference type
- Protocol
Answer: Reference type. Classes are reference types; assigning shares the same object.
Which keyword lets a struct method change its own properties?
- override
- static
- private
- mutating
Answer: mutating. A struct method that mutates its own state must be marked mutating.
A computed property...
- Calculates its value on each read with no storage
- Stores a value in memory
- Runs only once
- Cannot be read
Answer: Calculates its value on each read with no storage. A computed property has no storage; it runs code every time it's read.
Which types can conform to a protocol?
- Only classes
- Structs, classes, and enums
- Only structs
- Only enums
Answer: Structs, classes, and enums. Structs, classes, and enums can all conform to protocols.
Which keyword is required to replace an inherited method?
- super
- replace
- override
- final
Answer: override. You must mark a replacing method with override.
Can a struct inherit from another struct?
- Yes
- Only with a protocol
- Only one level
- No, only classes support inheritance
Answer: No, only classes support inheritance. Inheritance is a class-only feature; structs use protocols to share behaviour.
What does a property observer didSet do?
- Runs code after the property changes
- Computes the value
- Prevents changes
- Initialises the type
Answer: Runs code after the property changes. didSet runs automatically each time the stored property changes.
What gives every protocol conformer a default method for free?
- A subclass
- An initialiser
- A protocol extension
- A computed property
Answer: A protocol extension. A protocol extension supplies default implementations to all conformers.
Which call lets a subclass init run the parent's initialiser?
- parent.init()
- base.init()
- self.init()
- super.init()
Answer: super.init(). super.init(...) calls the parent class's initialiser.
Continue this course
- Previous: Functions and Closures
- Next: SwiftUI Basics — Build your first iOS screen using Apple's declarative UI framework
Frequently asked questions
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.
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.
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'.
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.
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.