Classes & Reference Types

Reviewed & published by Brayan K

Classes bring reference semantics, inheritance, and lifecycle hooks. By the end of this lesson you'll define classes, write init/deinit, use inheritance with override, and clearly see how a class differs from a struct.

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 in This Lesson

1️⃣ Defining a Class

Declare a class with its properties and methods. Unlike structs, classes have no free memberwise initializer — you write your own init to set up stored properties.

// A CLASS bundles data and behaviour — but unlike a struct it has no
// free memberwise initializer, so you write your own init.
class Dog {
    var name: String
    var age: Int

    init(name: String, age: Int) {     // designated initializer
        self.name = name
        self.age = age
    }

    func bark() {
        print("\(name) says woof!")
    }
}

let rex = Dog(name: "Rex", age: 3)
rex.bark()                              // Rex says woof!
print("\(rex.name) is \(rex.age)")    // Rex is 3

2️⃣ Reference Semantics (Shared Instances)

This is the heart of classes: assigning one to a new variable makes both refer to the same object. A change through one is seen through the other — the opposite of a struct's independent copy.

// REFERENCE SEMANTICS: variables SHARE the same instance.
class Counter {
    var count = 0
}

let a = Counter()
let b = a                  // b refers to the SAME object as a
b.count = 5                // change through b ...
print(a.count)             // 5  — a sees it too! (shared instance)
print(b.count)             // 5

// Note: 'a' is a let, yet we can still change a.count, because let fixes
// the REFERENCE, not the object's mutable properties.

// === Contrast with a struct (value type) ===
struct SCounter { var count = 0 }
var x = SCounter()
var y = x                  // y is a COPY
y.count = 5
print(x.count)             // 0  — copies are independent
print(y.count)             // 5

3️⃣ Lifecycle: init & deinit

init runs when you create an instance; deinit runs automatically just before the instance is freed — perfect for cleanup like closing files or removing observers.

// init runs when an instance is created; deinit runs just before it's freed.
class FileHandle {
    let name: String

    init(name: String) {
        self.name = name
        print("Opened \(name)")
    }

    deinit {
        print("Closed \(name)")        // cleanup before deallocation
    }
}

func useFile() {
    let f = FileHandle(name: "data.txt")
    print("Using \(f.name)")
    // when useFile() ends, f goes out of scope and deinit runs
}

useFile()

4️⃣ Inheritance & override

Only classes can inherit. A subclass extends a superclass and replaces inherited methods with override, enabling polymorphism — treating different subclasses through a shared base type.

// Classes support INHERITANCE. A subclass extends a superclass.
class Animal {
    var name: String
    init(name: String) { self.name = name }
    func speak() -> String { return "..." }
}

class Cat: Animal {                     // Cat inherits from Animal
    override func speak() -> String {   // 'override' replaces the parent method
        return "Meow"
    }
}

class Cow: Animal {
    override func speak() -> String { return "Moo" }
}

let animals: [Animal] = [Cat(name: "Tom"), Cow(name: "Bess")]
for a in animals {
    print("\(a.name): \(a.speak())")  // polymorphism in action
}

Your turn. Fill in the blanks, then run it and check the output.

// 🎯 YOUR TURN — replace each ___ then run it in SwiftFiddle.

// 1) Define a class 'Person' with a stored property 'name' (String).
___ Person {                      // 👉 replace ___ with the keyword class
    var name: String

    // 2) Write the initializer that sets self.name.
    init(name: String) {
        ___ = name                 // 👉 replace ___ with self.name
    }

    func greet() {
        print("Hi, I'm \(name)")
    }
}

let p = Person(name: "Ada")
p.greet()

// 3) Make a second variable that REFERENCES the same instance, then rename it.
let q = p
q.name = "Grace"
p.greet()                          // shows the shared change

// ✅ Expected output:
//    Hi, I'm Ada
//    Hi, I'm Grace

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

Featurestructclass
Semanticsvalue (copy)reference (shared)
Memberwise inityes (free)no (write your own)
Inheritancenoyes
deinitnoyes
Mutating keywordrequirednot needed
Identity (===)n/ayes

Mini-Challenge: Vehicles with Inheritance

Build it, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: Vehicles with inheritance
// 1. Define a class 'Vehicle' with a property 'wheels' (Int) and an init,
//    plus a method describe() -> String returning "A vehicle with N wheels".
// 2. Define 'Car: Vehicle' that overrides describe() to return
//    "A car with 4 wheels" (set wheels to 4 in its init via super.init).
// 3. Define 'Motorbike: Vehicle' similarly with 2 wheels.
// 4. Put a Car and a Motorbike in an [Vehicle] array and print each describe().
//
// ✅ Example output:
//    A car with 4 wheels
//    A motorbike with 2 wheels

// your code here

🎉 Lesson Complete!

Practice quiz

What keyword defines a class?

  • struct
  • object
  • class
  • type

Answer: class. Classes are declared with the class keyword.

Are classes value types or reference types?

  • Reference types
  • Value types
  • Neither
  • Both

Answer: Reference types. Classes are reference types — variables share the same instance.

Given let a = Box(); let b = a; b.value = 9 — what is a.value?

  • unchanged old value
  • nil
  • compile error
  • 9 (a and b share the same instance)

Answer: 9 (a and b share the same instance). Both a and b reference the same object, so a.value is also 9.

Do classes get a free memberwise initializer like structs?

  • Yes
  • No — you must write your own init
  • Only with @auto
  • Only for one property

Answer: No — you must write your own init. Classes require you to write an initializer for stored properties without defaults.

What is a deinitializer (deinit) used for?

  • Cleanup right before an instance is freed
  • Creating instances
  • Copying instances
  • Comparing instances

Answer: Cleanup right before an instance is freed. deinit runs just before a class instance is deallocated, for cleanup.

Which feature do classes support that structs do not?

  • Methods
  • Computed properties
  • Inheritance from another class
  • Protocols

Answer: Inheritance from another class. Only classes support inheritance from a superclass.

What keyword marks a method as overriding a superclass method?

  • super
  • override
  • virtual
  • replace

Answer: override. Subclasses use the override keyword to replace inherited methods.

Do you need 'mutating' to change a class property inside a method?

  • Yes, always
  • Only for let properties
  • Only in subclasses
  • No — classes are reference types, so it's not needed

Answer: No — classes are reference types, so it's not needed. mutating is a value-type concept; class methods can change properties freely.

Can you change a property of a class instance stored in a `let` constant?

  • No, never
  • Yes — let fixes the reference, not the object's var properties
  • Only the first property
  • Only with override

Answer: Yes — let fixes the reference, not the object's var properties. A let class reference is fixed, but the instance's var properties can still change.

When should you choose a class over a struct?

  • For all data
  • Never
  • When you need shared mutable state, identity, or inheritance
  • Only for numbers

Answer: When you need shared mutable state, identity, or inheritance. Classes suit reference semantics: shared state, identity, and inheritance.

Continue this course

Frequently asked questions

What is the difference between a class and a struct?

The core difference is semantics: classes are reference types and structs are value types. Assigning a class instance to another variable makes both point at the SAME object, so a change through one is visible through the other. Assigning a struct makes an independent copy. Classes also support inheritance, deinitializers, and identity checks (===); structs do not.

Why don't classes get a free memberwise initializer?

Because classes participate in inheritance, Swift can't safely synthesize a memberwise initializer the way it does for structs. You must write your own designated initializer that assigns every stored property without a default value. If a subclass adds properties, it provides its own init and calls super.init to set up the inherited part.

What is a deinitializer and when does it run?

A deinitializer (deinit) is special method on a class that runs automatically just before an instance is deallocated — when nothing references it any more. It's the place to release resources like file handles, observers, or timers. Structs don't have deinitializers because value types aren't reference-counted in the same way.

Can I change a property of a class instance held in a 'let'?

Yes — and this surprises people coming from structs. A let on a class fixes the reference (you can't point it at a different object), but the object's own var properties can still be changed. With a struct, a let makes the whole value immutable. This is a direct consequence of reference vs value semantics.

When should I use a class instead of a struct?

Choose a class when you need reference semantics: shared mutable state that several parts of your app see, object identity (is this the very same instance?), inheritance, or interoperability with Objective-C frameworks. For most data models, Apple recommends a struct; reach for a class deliberately when one of those reference-type features is genuinely required.