Module 2

Swift, the language

Swift is a statically typed, compiled, memory-safe language with strong functional leanings and an unusually expressive type system. It's closer to Rust or Kotlin than to JavaScript, but far friendlier than Rust about memory.

Value types vs reference types

This is the foundational distinction and everything else builds on it.

A struct is a value type. Assigning it copies it. Two variables holding the same struct are entirely independent.

struct Point { var x: Int; var y: Int }

var a = Point(x: 1, y: 2)
var b = a        // a full copy, not a shared reference
b.x = 99
// a.x is still 1. In JavaScript, this would surprise you.

A class is a reference type. Assigning it shares the same underlying object, exactly like a JavaScript object.

class Counter { var value = 0 }

let x = Counter()
let y = x
y.value = 5
// x.value is now 5 — same object.

When to use which: default to struct. Reach for class when you need shared mutable state (a controller, a store, a manager), when you need identity (two things that are "equal" but distinct), or when you're inheriting from an Apple class.

In Spoke: OverlayView is a struct because it's a description of UI. DictationController is a class because every part of the app must see the same instance. Transcriber is an actor — a class with thread-safety guarantees, covered in Module 3.

Trap · let vs var on structs

let on a struct makes the entire value immutable, including its properties. let p = Point(x:1,y:2); p.x = 5 is a compile error. On a class, let only means "this variable can't point at a different object" — you can still mutate its properties. This trips up nearly everyone once.

Optionals

Swift has no implicit null. A String is always a string. A String? is an Optional<String> — a box that either contains a string or is empty. You cannot use the value without opening the box, and the compiler enforces it.

Four ways to unwrap, in rough order of how often you'll use them:

// 1. guard let — the workhorse. Unwrap or bail out early.
func greet(_ name: String?) {
    guard let name else { return }
    // `name` is a non-optional String from here to the end of the function
    print("Hello, \(name)")
}

// 2. if let — unwrap for a narrow scope.
if let name { print(name) }

// 3. ?? — supply a default.
let display = name ?? "stranger"

// 4. ?. — optional chaining. The whole expression becomes optional.
let length = name?.count   // Int?  — nil if name was nil

guard deserves special attention because it's the idiom you'll type most. It says "this must be true to continue; otherwise leave now." Unlike if, the unwrapped value stays in scope after the guard, which keeps the happy path un-nested. It's the single biggest readability win in Swift.

Trap · force unwrapping

name! forcibly opens the box and crashes the app if it's empty. It has legitimate uses (a resource you shipped in the bundle and know exists), but treat every ! as a claim you're making to the compiler that you'd better be able to defend. In production code it's usually a bug waiting for a user to find.

Closures and capture lists

Closures are anonymous functions that capture their surrounding scope — same as JavaScript arrow functions or Dart closures. Swift's syntax has one quirk worth internalising: trailing closure syntax.

// These three are identical:
audio.start(onBuffer: { buffer in handle(buffer) })
audio.start { buffer in handle(buffer) }
audio.start { handle($0) }   // $0 is the first parameter

If the closure is the last parameter, it can move outside the parentheses. This is why SwiftUI reads the way it does — Button("Save") { save() } is just a function call with a trailing closure.

The part that matters for correctness is the capture list: the [weak self] you see everywhere.

hotkey.onPress = { [weak self] in
    Task { await self?.beginListening() }
}

By default a closure holds a strong reference to everything it captures, keeping those objects alive. If an object holds a closure that captures that same object, neither can ever be freed — a retain cycle, which is Swift's version of a memory leak. [weak self] makes the capture non-owning, so self becomes an optional that turns nil if the object goes away.

Rule of thumb: if the closure is stored somewhere and called later (a callback, an event handler, a subscription), use [weak self]. If the closure runs immediately and is discarded (map, filter, sort), you don't need it.

Protocols and extensions

A protocol is an interface: a list of requirements a type promises to satisfy. If you know TypeScript interfaces or Dart abstract classes, you're most of the way there.

protocol Describable {
    var summary: String { get }
    func reset()
}

struct Session: Describable {
    var summary: String { "a session" }
    func reset() { }
}

What makes protocols more powerful than most interface systems is that you can add default implementations via extensions, and you can extend types you don't own:

// Give every Describable a free implementation
extension Describable {
    func reset() { print("nothing to reset") }
}

// Add behaviour to a type from another module — even from Apple
extension String {
    var isBlank: Bool { trimmingCharacters(in: .whitespaces).isEmpty }
}

This is why Swift codebases lean on protocols rather than inheritance. View, App, Identifiable, Codable, Sendable — the entire SwiftUI surface is protocols with default implementations.

Enums with associated values

Swift enums are not C enums. They can carry data, which makes them the natural way to model state.

enum State: Equatable {
    case settingUp
    case idle
    case listening
    case polishing
    case error(String)     // this case carries a message
}

Two properties make this powerful. First, illegal states become unrepresentable — you cannot be both listening and polishing, because it's one value. Compare that to three booleans, where four of the eight combinations are nonsense. Second, switch is exhaustive: add a case and every switch that doesn't handle it fails to compile. Refactoring becomes a conversation with the compiler instead of a hunt.

switch state {
case .settingUp:       return "Setting up…"
case .idle:            return "Ready"
case .listening:       return "Listening…"
case .polishing:       return "Cleaning up…"
case .error(let msg):  return msg   // bind the payload
}

Error handling

Swift uses typed, checked exceptions — but unlike Java, only at function boundaries you explicitly mark.

enum CaptureError: LocalizedError {
    case noInputDevice

    var errorDescription: String? {
        switch self {
        case .noInputDevice: return "No microphone available."
        }
    }
}

func start() throws {          // marked as able to throw
    guard hasDevice else { throw CaptureError.noInputDevice }
}

do {
    try start()               // `try` is mandatory and visible
} catch {
    print(error.localizedDescription)   // `error` is implicit
}

Three variants worth knowing: try propagates the error, try? converts it to an optional (nil on failure), and try! crashes on failure. The visible try at every call site is deliberate — you can scan a function and immediately see every place it can fail.

Conforming to LocalizedError rather than plain Error is what makes error.localizedDescription produce your message instead of a useless generic one. Spoke does this everywhere, because those strings go straight into the overlay where users see them.

Property wrappers and macros

The @ symbols everywhere are one of two things.

Property wrappers (@State, @Binding) wrap a property with extra behaviour. @State var count = 0 isn't just an integer — it's a struct that stores the value outside the view and tells SwiftUI to re-render when it changes.

Macros (@Observable, @Generable, @main) generate code at compile time. This is genuinely new in Swift and worth understanding, because it explains behaviour that would otherwise look like magic.

@Observable
final class DictationController {
    var state: State = .idle
}

At compile time, @Observable rewrites this class so every property has hidden access-tracking. When a SwiftUI view reads controller.state during rendering, the framework records that dependency; when state is written, only views that actually read it re-render. It's finer-grained than React's re-render-the-subtree default and you get it for free.

In Xcode you can right-click any macro and choose Expand Macro to see the generated source. Do this once — it demystifies the whole system.

Memory and retain cycles

Swift uses ARC (Automatic Reference Counting), not a tracing garbage collector. Every reference-type instance carries a count of strong references to it; when that count hits zero, the object is deallocated immediately and deterministically.

The upside: no GC pauses, predictable memory, immediate cleanup (deinit runs exactly when you'd expect). The downside: cycles are never collected. If A strongly references B and B strongly references A, both leak forever.

Three reference strengths:

KeywordKeeps object alive?Becomes nil?Use for
strong (default)YesNoOwnership
weakNoYes — must be optionalBack-references, delegates, callbacks
unownedNoNo — crashes if accessed after deallocWhen you're certain the target outlives you

In practice: use strong by default, weak in stored closures and delegate properties, and avoid unowned until you have a specific reason. Xcode's Memory Graph Debugger (the little graph icon while running) will show you cycles visually if you suspect one.

Spoke never sends your voice, your text, or anything else off this Mac. Not a promise in a privacy policy — a property of the binary.