1.6 — Structs, classes, enums, protocols (the four pillars)
Opening scenario
You join a code review and find this PR:
class User {
var id: UUID
var name: String
var email: String
init(id: UUID, name: String, email: String) { /* ... */ }
}
You leave a review comment: “Should this be a struct?”
The author responds: “Why does it matter?”
How you answer that question — in your head, in a PR, in an interview — defines whether you’re a Swift programmer or a Java/Kotlin programmer typing Swift.
The taxonomy
Swift has named types in four flavors:
| Kind | Value or reference? | Inheritance? | Best for |
|---|---|---|---|
struct | Value | No | Data models, view state, anything immutable-ish |
class | Reference | Yes (single) | Identity, shared mutable state, ObjC interop |
enum | Value | No | Closed sets of cases, state machines, results |
actor | Reference (isolated) | No | Concurrency-safe mutable state (Chapter 1.9) |
Plus protocol — not a type itself but a contract a type can adopt — which is what makes Swift’s OOP feel different from Java’s or Kotlin’s.
We’ll cover all of these, then end on the question every Swift engineer has to answer: struct or class?
Concept → Why → How → Code
Structs: the default
struct Point {
var x: Double
var y: Double
func distance(to other: Point) -> Double {
let dx = x - other.x
let dy = y - other.y
return (dx*dx + dy*dy).squareRoot()
}
// mutating methods must say so explicitly
mutating func translate(by delta: Point) {
x += delta.x
y += delta.y
}
}
let a = Point(x: 0, y: 0) // memberwise init for free
var b = a // COPY, not a reference
b.x = 5
print(a.x) // 0 — a unchanged
Why value semantics matter: when you pass a Point to a function, the function gets a copy. It cannot mutate your Point behind your back. Local reasoning becomes possible.
Classes: when you need identity
class ViewModel {
var items: [Item] = []
func reload() { /* ... */ }
}
let vm1 = ViewModel()
let vm2 = vm1 // SAME instance — both point to the same object
vm2.items.append(Item())
print(vm1.items.count) // 1 — they share state
// Equality: === is reference identity, == is value equality (if Equatable)
print(vm1 === vm2) // true
You reach for class when:
- You need identity (two
Userinstances with the same name are still different users in your domain). - You need inheritance (a
UIViewControllersubclass). - You’re interoperating with Objective-C (NSObject subclass).
- You need shared mutable state with reference semantics (a cache, a coordinator).
Enums: pattern matching is the point
Swift enums are dramatically more powerful than C/Java enums. They carry associated values and support methods, computed properties, even protocols.
enum LoadState<T> {
case idle
case loading
case loaded(T)
case failed(Error)
var isFinished: Bool {
switch self {
case .loaded, .failed: true
case .idle, .loading: false
}
}
}
let state: LoadState<[Post]> = .loaded([])
switch state {
case .idle: print("waiting")
case .loading: print("...")
case .loaded(let xs): print("got \(xs.count)")
case .failed(let e): print("error: \(e)")
}
This is the single most powerful Swift feature for modeling domain state. Bad code says var isLoading: Bool, var data: [Post]?, var error: Error?. Good code says var state: LoadState<[Post]>.
Raw values (when each case has a primitive underlying value):
enum HTTPStatus: Int {
case ok = 200
case notFound = 404
case serverError = 500
}
let s = HTTPStatus(rawValue: 404) // HTTPStatus? — Optional
Protocols: contracts that types adopt
protocol Drawable {
func draw(in context: GraphicsContext)
var bounds: CGRect { get }
}
struct Circle: Drawable {
let center: CGPoint
let radius: Double
var bounds: CGRect { CGRect(x: center.x - radius, /* … */ ) }
func draw(in ctx: GraphicsContext) { /* … */ }
}
extension Array where Element == Drawable {
func drawAll(in ctx: GraphicsContext) {
forEach { $0.draw(in: ctx) }
}
}
Protocols are what types can be expected to do; structs/classes/enums are how that’s delivered. Functions can require Drawable instead of caring what kind of thing they got.
Protocol extensions: behavior with no inheritance
Java/Kotlin extract shared behavior via an abstract base class. Swift uses protocol extensions:
protocol Greetable {
var name: String { get }
}
extension Greetable {
func greet() -> String { "Hello, \(name)" } // default implementation
}
struct Person: Greetable { let name: String }
Person(name: "Ada").greet() // "Hello, Ada"
This is protocol-oriented programming — the design ethos Apple promoted hard at WWDC 2015. Compose behavior into small protocols; let concrete types adopt the protocols they need; share implementations via extensions.
In the wild
Codableis a protocol (composed ofEncodableandDecodable). Conform your model struct and free JSON encoding/decoding appears via the compiler-generated implementation.Identifiable,Hashable,Equatable— used everywhere in SwiftUI’sForEach, inSet, inDictionarykeys. The compiler can synthesize all three.Viewin SwiftUI is a protocol, not a class. Every SwiftUI view is a struct (yes, structs!) conforming toView.Sendable— the Swift 6 concurrency protocol that marks types safe to cross actor boundaries.- MVVM/MVI architectures: the M (model) is usually a struct, the VM (view model) is usually a class with
ObservableObjector@Observable, the V (view) is a struct.
Common misconceptions
-
“Classes are more ‘real’ OOP than structs.” This is Java thinking. In Swift the default is struct; classes are a specialization for when you need their unique features.
-
“Structs are slow because they copy.” COW (copy-on-write) makes struct copies cheap for the standard collections. For your own structs, copying is just member-wise — small. The compiler also optimizes returns to avoid copies.
-
“You can’t have polymorphism with structs.” False — protocols give you polymorphism without inheritance.
func render(_ shapes: [any Drawable])accepts circles, squares, paths, all heterogeneous. -
“
enumis for fixed lists likeColor { red, green, blue }.” That’s the C view. In Swift, enums with associated values are the canonical way to model “one of these N possibilities, each with different data.” -
“
protocols are just Java interfaces.” Similar, but with two key differences: (a) protocols can have default implementations (extensions), and (b) protocols can constrain associated types (we’ll see this in Generics).
Seasoned engineer’s take
Apple’s official advice is: start with a struct. Move to a class only when you have a reason. Reasons:
- You need identity — two distinct objects with identical fields are different (a
Userin your domain, a network session). - You need inheritance — typically because UIKit/AppKit forces it on you.
- You need shared mutable state with reference semantics (a cache, an in-memory store).
- You need Objective-C interop (
NSObjectsubclass for KVO, NSCoding, etc.).
For everything else — your Article, your BlogPost, your Profile, your view-state, your DTOs from the network — use struct. Value semantics + protocol conformance is the modern Swift idiom.
A heuristic I find useful: does it make sense to compare two instances with ==? If “same data = equal” is your domain rule, struct. If “different objects, even with same data” is the rule, class. (User(id: 1, name: "Ada") == User(id: 1, name: "Ada") → true makes sense, so User is a struct.)
The hard cases:
- Big structs (>200 bytes). Pass-by-value is still cheap (Swift uses register passing where possible), but if you’re holding millions of them, profile.
- Recursive types (a
Nodewithvar children: [Node]). Structs work fine for immutable trees; for mutable recursive structures, classes are often less surprising. - Long-lived state that must be unique (like a coordinator object that owns navigation). Classes with
finalkeyword.
TIP: Conform your structs to
Equatable,Hashable,Codableproactively. The Swift compiler synthesizes them for free if every stored property already conforms. It costs you nothing and unlocksSet,Dictionarykeys,ForEach, JSON I/O.
WARNING: Inheritance with classes is a slippery slope. Three levels deep and you’ll wish you’d composed protocols instead. Apple has explicitly stated the modern Swift recommendation is composition over inheritance. Use
final classby default — most classes should be unsubclassable unless they’re explicitly designed to be inherited.
Interview corner
Question: “When would you use a class instead of a struct in Swift?”
Junior answer: “When I need inheritance or when I want shared mutable state.” → Correct, but textbook. They’ll push.
Mid-level answer: “I default to struct for value-semantic models (anything that’s just data). I use class when I need (a) reference identity — like a long-lived ViewModel that the view holds a reference to, (b) inheritance — usually forced by UIKit, (c) Objective-C interop, or (d) when the type is genuinely a thing in the world rather than a value — a cache, a network session manager. With SwiftUI specifically, my models are structs, my view models are @Observable classes.” → Strong.
Senior answer: All of that, plus: “The deeper question is about identity vs. value. User is interesting because reasonable people disagree. Some teams treat User as a value (two Users with the same id are equal, immutable snapshots). Other teams treat User as having identity (the User object you’re holding is the user, mutations propagate). I’d ask: do we need to observe changes to this object in many places? Do we share mutation with state-management infrastructure (@Observable, Redux store)? If yes → class. If we’re passing snapshots around (network DTOs, view state) → struct. I’d also point out that the answer evolves: a User struct + a UserSession class is often cleaner than one giant User class doing both jobs.” → Senior signal: distinguishes data from identity.
Red-flag answer: “Classes are better because they’re faster.” → Both wrong (structs are often faster due to stack allocation and inlining) and outs you as someone who’s never profiled.
Lab preview
Lab 1.C (Protocol-oriented calculator) builds an arithmetic library where every operation is a struct conforming to a BinaryOperation protocol. You’ll see protocol-oriented design in 80 lines.
Next: Swift’s most powerful and most intimidating feature — generics. → Generics and the type system