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

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)")     // 99

2️⃣ 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: 15

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)")   // true

4️⃣ 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-D2

Pro Tips

Common Errors (and the fix)

📋 Quick Reference — struct vs class

Featurestruct (value type)class (reference type)
On assignmentCopied (independent)Shared (same object)
InheritanceNoYes (single)
Change own property in a methodNeeds mutatingJust works
Free initialiserYes (memberwise)No — write init
Override a methodN/Aoverride func ...
Use it when…Default choice; simple dataShared 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!

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

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.