Skip to content
C.W.K.
Stream
Lesson 01 of 06 · published

Values, References, and Types That Refuse Wrong Answers

~15 min · swift-for-apple, structs, classes, enums, modeling

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"If a value's meaning depends on where it is, put the where in the type."

Structs for Facts, Classes for Things With Identity

A Swift struct is a value: copy it and you have two independent facts. A class is a reference: copy the variable and both names point at one object. That difference decides most modeling choices on Apple platforms.

  • Your data is values. A capture, a settings document, a wire message — structs. They are Sendable for free when their fields are, they compare with == once they declare Equatable, and they cannot be mutated behind your back by another part of the app.
  • The frameworks' long-lived objects are classes. Windows, views, view controllers, delegates, a WatchConnectivity session — every AppKit and UIKit object that sits in the responder chain is an NSObject subclass, because its identity is the point. The view you configured is the view on screen.
  • Observable models are classes. The @Observable macro applies to classes, because a view needs to watch one shared instance change over time.

Mark a class final unless you mean it to be subclassed. It documents intent, and the compiler can dispatch its methods directly.

An Enum That Makes the Wrong Call Impossible

A desktop client in this family had a files-and-terminal dock that bound itself to "the session's workspace path" — a plain URL. On a fleet where the same absolute repository path exists on several Macs, a session running on another machine made the dock show and edit the local checkout, while the model worked on the remote host. Everything looked correct, because a URL cannot say which machine it belongs to.

The fix was not a check at each call site. The dock's binding stopped accepting a bare URL and accepted a two-case enum instead: .local(URL) or .unavailable(reason:). After that, no caller could even express "this path, host unknown". Later, when remote sessions needed a working dock, the type grew a third case, .remote(socket), and not a single call site changed — every surface already routed through the one decision.

That is the modeling habit worth stealing: the type you add to refuse a wrong answer is where the right answer goes later. Enums with associated values, switch statements the compiler checks for exhaustiveness, and structs whose initializers refuse impossible states turn a class of runtime bugs into compile errors.

Where Swift Meets Objective-C

Swift enums with payloads do not exist in Objective-C, and neither do generic structs. When a framework API is Objective-C underneath, you will hand it classes, @objc protocols and simple types, and keep the rich Swift model on your side of the boundary. That boundary is a theme of this quest: it is where threading assumptions, nullability and runtime instantiation rules cross over.

Code

Spark's capture destination: the where lives in the type·swift
import Foundation

struct Capture: Codable, Equatable, Sendable {
    let id: String          // the idempotency key the engine dedupes on
    var text: String
}

enum CaptureDestination: Equatable {
    case local(URL)
    case remote(host: String, port: Int)
    case unavailable(reason: String)
}

func folderLabel(_ destination: CaptureDestination) -> String {
    switch destination {                       // the compiler insists every case is handled
    case .local(let url):              return url.lastPathComponent
    case .remote(let host, let port):  return "\(host):\(port)"
    case .unavailable(let reason):     return "unavailable — \(reason)"
    }
}

final class CaptureQueueModel {   // one shared instance a view watches: a class
    private(set) var pending: [Capture] = []
    func enqueue(_ capture: Capture) {
        guard !pending.contains(where: { $0.id == capture.id }) else { return }
        pending.append(capture)
    }
}

External links

Exercise

Add the CaptureDestination enum and the Capture struct to your Spark package. Then find one place in any code you have written where a plain String or URL secretly depends on context (which server, which user, which time zone). Redesign it as an enum or struct so the context cannot be forgotten, and list every call site the compiler now forces you to revisit.
Hint
A good sign you need this: comments like "assumes local", "only valid after login", or "in UTC" next to a primitive. Each of those assumptions wants to be a case or a field.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.