Skip to main content
    Courses/Swift/Object-Oriented Programming

    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.

    Worked example: a copied struct vs a shared class
    // 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
    Output
    pointA = (3, 5)
    pointB = (99, 5)
    counterA.value = 99
    counterB.value = 99
    This is real code — run it for free atSwiftFiddleor in your own editor.

    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.

    Worked example: celsius (stored + didSet) and fahrenheit (computed)
    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
    Output
    Start: 20.0°C = 68.0°F
    Temp changed: 20.0°C -> 100.0°C
    Now:   100.0°C = 212.0°F
    This is real code — run it for free atSwiftFiddleor in your own editor.

    🎯 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: add the mutating keyword and the += operator
    // 🎯 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
    Output
    Sam scored! Total: 10
    Sam scored! Total: 15
    Replace each ___ 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.

    Worked example: Dog subclasses Animal and overrides speak()
    // 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
    Output
    Creature makes a sound
    Rex the Corgi says Woof!
    Rex is still an Animal: true
    This is real code — run it for free atSwiftFiddleor in your own editor.

    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.

    Worked example: a Shape protocol with a default describe()
    // 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()
    }
    Output
    Square has area 16.0
    Circle has area 12.56
    This is real code — run it for free atSwiftFiddleor in your own editor.

    🎯 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: adopt Greetable and implement greet()
    // 🎯 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
    Output
    Beep boop, I am R2-D2
    Replace each ___ 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 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

    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

    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: design Vehicle, Car, and Motorcycle
    // 🎯 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
    Write the code yourself, then run it at SwiftFiddle and compare with the example output in the comments.

    🎉 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
    • 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service