Chapter 81: Swift Class vs Struct
Class vs Struct in Swift — written exactly like a patient, experienced teacher sitting next to you with a playground open.
We are going to go very slowly, with many small examples, real-life analogies, real-world decision patterns, common beginner mistakes, and the modern mental model that most good Swift developers use every day in 2025–2026.
Let’s start from the very beginning.
1. The Core Difference — Value Type vs Reference Type
This is the single most important thing to understand. Everything else follows from this.
| Property | Struct (Value Type) | Class (Reference Type) | Real-life analogy |
|---|---|---|---|
| What is assigned / passed | Full copy of all data | Reference (pointer) to the same object | Giving a photocopy vs giving the original photo |
| Changing one variable affects others? | No — each copy is independent | Yes — all variables point to the same object | Changing one photocopy doesn’t change others |
| Identity comparison (===) | Not possible (value types don’t have identity) | Possible — checks if same instance in memory | Asking “are these two the exact same photo print?” |
| Memory management | Copied on write (Copy-on-Write for Array/Dict/String) | Reference counting (ARC) | Copies take more memory only when actually changed |
| Thread safety (mutable shared state) | Safer — copies are independent | Dangerous — needs careful synchronization | Copies are safe to modify from multiple threads |
| Performance (small data) | Usually faster (stack allocation possible) | Usually slower (heap + reference counting) | Small copies are cheap |
| Can inherit | No | Yes (single inheritance) | Classes can have parents |
| Can be used as Dictionary key | Yes (if Hashable) | Yes (but need careful Hashable) | Structs are easier for keys |
Golden rule used by almost every experienced Swift developer today:
Default to struct for almost everything. Use class only when you really need one of these things:
- reference semantics (shared mutable state)
- inheritance
- Objective-C interop / UIKit compatibility
- identity comparison (===)
2. Side-by-side comparison with real examples
Example 1 – Mutability & copying
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Struct — value type struct Point { var x: Int var y: Int } var p1 = Point(x: 10, y: 20) var p2 = p1 // ← full copy p2.x = 100 print(p1.x) // 10 — unchanged print(p2.x) // 100 — only p2 changed |
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Class — reference type class ReferencePoint { var x: Int var y: Int init(x: Int, y: Int) { self.x = x self.y = y } } let rp1 = ReferencePoint(x: 10, y: 20) let rp2 = rp1 // ← same object, just another reference rp2.x = 100 print(rp1.x) // 100 — changed! print(rp2.x) // 100 — same object print(rp1 === rp2) // true — same instance |
Key takeaway:
- Struct → changing one copy never affects others
- Class → changing through any reference affects all references
3. Real-life examples — when developers choose struct vs class
Example 1 – Data model (almost always struct)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct Product { let id: String let name: String let price: Decimal var quantity: Int = 1 var subtotal: Decimal { price * Decimal(quantity) } mutating func increase(by step: Int = 1) { quantity += step } } var cart: [Product] = [] cart.append(Product(id: "P001", name: "Earbuds", price: 3499)) // Copying is safe var backupCart = cart backupCart[0].increase(by: 2) print(cart[0].quantity) // still 1 — safe! print(backupCart[0].quantity) // 3 |
Why struct here?
- No shared mutable state needed
- Copying is cheap (Copy-on-Write for Array)
- Value semantics — very predictable & safe
Example 2 – ViewController / view model (often class)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CartViewModel: ObservableObject { @Published var items: [CartItem] = [] @Published var total: Decimal = 0 func addItem(_ item: CartItem) { items.append(item) updateTotal() } private func updateTotal() { total = items.reduce(0) { $0 + $1.subtotal } } } |
Why class here?
- Needs to be shared between views (@ObservedObject, @StateObject)
- Identity matters (same view model instance)
- Needs to notify observers when data changes (@Published)
3. When to choose struct vs class — decision checklist
| Question / Need | Choose Struct | Choose Class | Typical real-world decision |
|---|---|---|---|
| Do you need identity (===)? | No | Yes | Shared state, view models |
| Do you need inheritance? | No | Yes | UIKit/AppKit view controllers |
| Do you want value semantics (copy = independent)? | Yes | No | Data models, DTOs, value objects |
| Is thread-safety important (mutable shared state)? | Yes (safer) | No (needs careful synchronization) | Modern apps prefer struct |
| Is the data small & frequently copied? | Yes (Copy-on-Write efficient) | No (reference counting overhead) | Almost always struct |
| Do you need Objective-C interop? | No | Yes | UIKit, Core Data, etc. |
| Are you using SwiftUI / Combine heavily? | Yes (structs + @State / @Binding) | Sometimes (ObservableObject class) | Structs for most data |
Modern Swift recommendation (2025–2026):
Default to struct for almost everything Use class only when you really need:
- reference semantics (shared mutable state)
- inheritance
- Objective-C compatibility
- identity comparison (===)
4. Very Common Beginner Mistakes & Correct Habits
| Mistake | Wrong / Risky code | Correct / Better habit | Why? |
|---|---|---|---|
| Using class by default “just in case” | class User { … } everywhere | Default to struct unless you need reference semantics | Value types are safer & more predictable |
| Force-unwrapping in class/struct | self.user!.name | guard let user = user else { … } | Prevents crashes |
| Deep class inheritance | 5+ levels of class inheritance | Prefer composition or protocols | Deep hierarchies become hard to reason about |
| Forgetting mutating in struct methods | func increaseAge() { age += 1 } | mutating func increaseAge() { age += 1 } | Compile error if missing |
| Sharing mutable class state without care | static let shared = MyManager() | Prefer dependency injection over singletons | Singletons make testing & reasoning hard |
5. Small Practice — Try these
- Create a Personstruct with:
- let name: String
- var age: Int
- mutating func haveBirthday()
- Create a BankAccountstruct like the example above
- private var balance: Double
- init(initialDeposit:)
- mutating func deposit(amount:)
- mutating func withdraw(amount:) -> Bool
- Create a Locationstruct
- let latitude: Double, longitude: Double
- var name: String?
- computed property coordinateString
Paste your code here if you want feedback or want to see more polished versions!
What would you like to explore next?
- Struct vs Class — detailed comparison & decision guide
- Mutability inside structs (mutating methods)
- Struct initializers (default, custom, memberwise)
- Structs in SwiftUI (@State, value types, performance)
- Or move to another topic (optionals, arrays, closures, switch…)
Just tell me — we’ll continue in the same clear, detailed, patient style 😊
