"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
Sendablefor free when their fields are, they compare with==once they declareEquatable, 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
NSObjectsubclass, because its identity is the point. The view you configured is the view on screen. - Observable models are classes. The
@Observablemacro 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.