iOS / Swift / Swift values and SwiftUI ownership / Lesson 2
Value models, collections, and errors
Use structs and enums to preserve invariants, and understand where collection copying stops protecting shared references.
On this page
Lesson goals
- Choose values for independent snapshots
- Make invalid transitions difficult to express
- Demonstrate snapshot independence
Choose values for independent snapshots
A struct has value semantics: assigning or passing it conceptually produces an independent value. Swift can optimize storage through copy-on-write for collections without changing that observable contract. A class has reference semantics, so two variables can refer to the same instance. Choose based on identity and ownership needs, not a blanket rule that one is faster. Small immutable records are useful inputs for views because a snapshot is easy to reason about. A struct containing a reference to a mutable class still shares that referenced object unless the model deliberately copies or isolates it.
Make invalid transitions difficult to express
A model should name its quantities and establish the invariants callers may rely on. The first fragment defines a workshop with a nonnegative seat count and an operation that can fail when capacity is insufficient. The mutating keyword makes the value-changing method explicit. Throwing a domain error communicates that failure without inventing a partially successful record. The model does not enforce server authorization or concurrent capacity across clients; it describes a local synthetic rule. Remote commands need an authoritative service boundary even when the local value model is perfectly validated.
enum SeatError: Error { case invalidCount, insufficientCapacity }
struct Workshop {
let id: String
private(set) var seats: Int
init(id: String, seats: Int) throws {
guard !id.isEmpty, seats >= 0 else { throw SeatError.invalidCount }
self.id = id; self.seats = seats
}
mutating func reserve(_ count: Int) throws {
guard count > 0 else { throw SeatError.invalidCount }
guard count <= seats else { throw SeatError.insufficientCapacity }
seats -= count
}
}
Accepted models have nonempty identity and nonnegative seats; invalid reservations throw before mutation.
private(set) allows callers to read seats while restricting direct writes. The example is a local model, not a real reservation service.
Demonstrate snapshot independence
The second fragment copies a workshop value, reserves in the copy, and prints both seat counts. The original remains unchanged. This is valuable when creating an editable draft from saved data: cancel can discard the draft instead of reconstructing the previous model. Define the actual save event so a draft does not silently become authoritative. Error handling belongs where recovery can be chosen, and a failed reservation should leave the accepted value coherent. Avoid using try! to make a teaching example shorter when failure is part of the domain contract.
do {
let original = try Workshop(id: "w-7", seats: 5)
var draft = original
try draft.reserve(2)
print(original.seats)
print(draft.seats)
} catch {
print("Reservation rejected")
}
5
3
The fragment depends on Workshop from the first example. No remote capacity or transaction is changed.
Use collection operations with a stable ordering rule
Array preserves element order; Dictionary provides key-based access but should not be used as a substitute for an explicit user-visible sort policy. Set expresses membership without duplicate elements and is useful for selected identifiers. Choose the collection that matches the operation rather than repeatedly converting between them in each row. A filter preserves the source arrayโs relative order, while a sort needs a comparator with coherent ordering. If two labels compare equally, decide whether a secondary stable identifier should determine order so refreshed data does not appear to jump unpredictably.
Use enums for meaningful alternatives
An enum can express a loading, loaded, or failed state more clearly than several booleans whose combinations include contradictions. Associated values can carry the data or error context belonging to each case. A switch then forces presentation to consider the alternatives exposed by the type. Resist putting unrelated concerns into one giant enum; model orthogonal state separately when combinations are genuinely valid. For example, a refresh can be active while previous results remain visible, so the state contract should explain whether loading carries earlier data instead of assuming loading always means an empty screen.
Test invariants at every mutation boundary
Useful model checks include constructing zero seats, rejecting a negative count, reserving exact capacity, rejecting an excessive request, and confirming failure leaves the prior value unchanged. Also test a draft copy to establish the intended independence. If the model later contains mutable reference objects, add a nested-ownership check rather than assuming struct syntax guarantees deep copying. Keep these tests pure and synthetic. Their success would establish local invariants, while distributed conflicts, server permissions, and durable persistence remain separate integration concerns requiring separate evidence.
Common errors and fixes
A copied struct still changes shared nested data
- Why it happens
- A field refers to the same mutable class instance.
- How to fix it
- Use immutable nested values or an explicit ownership/copy policy.
- Prevent it next time
- Test nested mutation, not only top-level assignment.
Failure is converted to a fabricated success
- Why it happens
- try? or a default erased the error distinction.
- How to fix it
- Expose a meaningful optional or error outcome appropriate to recovery.
- Prevent it next time
- Review every lossy error conversion.
Refreshing rows changes their visible order arbitrarily
- Why it happens
- No explicit ordering policy was defined for the collection.
- How to fix it
- Sort by documented fields with a stable tie-breaker where needed.
- Prevent it next time
- Test equal labels and reordered input fixtures.
Check your understanding
1. Why should reserve check count before subtracting?
Show worked solution
The operation must either produce another valid value or fail without changing the accepted state. Checking positivity and capacity first preserves nonnegative seats and avoids repairing a partially mutated value in catch logic. The invariant belongs to the model operation itself.
2. When is a Set preferable to an Array for selected workshop identifiers?
Show worked solution
When selection represents unique membership and display order is not stored there. A Set naturally prevents duplicates and supports membership checks. Derive visible order from the current ordered workshop array; if selection order itself matters, choose a representation that records it explicitly.
Key takeaways
- Choose values for independent snapshots
- Test invariants at every mutation boundary