Real apps hold real things: an expense, a user, a recipe. Swift gives you several ways to model them. Picking the right one is a skill, and it is a very common interview topic. This lesson builds the data model that the Expense Tracker uses later.
Structs
A struct groups related values and the functions that work on them. You make a new one by calling its name with the values. Swift gives every struct a free memberwise initializer, so you do not have to write the set-up code yourself.
struct Expense {
var title: String
var amount: Double
var category: String = "General"
func summary() -> String {
"\(title): \(amount)"
}
}
let lunch = Expense(title: "Lunch", amount: 8.5)
print(lunch.summary()) // Lunch: 8.5
Notice the function summary() has no return. When a function body is a single expression, Swift returns it for you. Also, lunch is a let, so none of its properties can change. If you need to change one, declare it with var.
Value types and reference types
This is the idea to understand well. Structs are value types. When you assign a struct to a new name, or pass it to a function, you get your own *copy*. Changing the copy does not change the original. Classes are reference types. A copy of a class variable points to the *same* object, so a change through one name shows up through the other.
struct PointS { var x = 0 }
class PointC { var x = 0 }
var a = PointS(); var b = a
b.x = 5
print(a.x, b.x) // 0 5, two separate copies
let c = PointC(); let d = c
d.x = 5
print(c.x, d.x) // 5 5, one shared object
- Use a struct by default. Copies are easy to reason about, and SwiftUI is built around structs.
- Use a class when you need one shared object that many parts of the code change, or when a framework requires it. Classes can also inherit from other classes, which structs cannot do.
- A class needs an
initthat sets every property, unless all of them have default values. - A struct that must change its own properties inside a method marks that method
mutating.
Enums
An enum is a type with a fixed set of cases. It is perfect for things that can be one of a few options, such as an expense category. Cases can even carry extra values.
enum Category: String, CaseIterable {
case food = "Food"
case travel = "Travel"
case fun = "Fun"
}
for c in Category.allCases {
print(c.rawValue)
}
enum LoadState {
case loading
case loaded(count: Int)
case failed(message: String)
}
let state = LoadState.loaded(count: 3)
switch state {
case .loading: print("Loading")
case .loaded(let count): print("Loaded \(count)")
case .failed(let message): print(message)
}
CaseIterable gives you allCases, a list of every case, which is ideal for building a picker. The LoadState enum is a very common pattern. One value tells a screen whether to show a spinner, a list or an error message, and the switch makes you handle every case.
Protocols
A protocol is a list of requirements, the properties and methods a type must have. Any type that meets the list can say it *conforms* to the protocol. Code that asks for the protocol does not care which type it receives, as long as the type can do the job.
protocol Summarizable {
var title: String { get }
func summary() -> String
}
extension Expense: Summarizable {} // Expense already has both
func printAll(_ items: [any Summarizable]) {
for item in items { print(item.summary()) }
}
printAll([lunch, Expense(title: "Bus", amount: 2)])
Two protocols appear in SwiftUI work again and again. Identifiable says a type has a stable id, so a list can tell items apart. Codable lets a type turn into and out of JSON, which is lesson 11. Protocols also make testing easier, because you can swap a real part for a simple fake one in lesson 13.
Extensions
An extension adds new methods or computed properties to a type that already exists, even one you did not write. It is a tidy way to keep code grouped.
extension Double {
var asMoney: String { String(format: "%.2f", self) }
}
print(12.5.asMoney) // 12.50
Choosing between them
- Data that describes a thing: a struct.
- One of a few options: an enum.
- A shared object with its own life, such as a manager that many screens use: a class.
- A promise of behaviour, so different types can be swapped: a protocol.
Your task
- Make an
Expensestruct withid(aUUID()),title,amountand aCategoryenum. Make it conform toIdentifiable. - Write an extension on
Array<Expense>with atotalproperty. - Prove the difference between a struct and a class copy with your own tiny example, and write one sentence about what you saw.
- Make an enum
LoadStateand a function that returns a message for each case.
How to check it worked
- Your
totalproperty adds up amounts correctly. - Changing a copy of the struct leaves the original unchanged, and changing a copy of the class changes both.
- You can say when you would choose a struct and when a class.