The model: expenses
What you'll learn:
- build an expense-tracking model from an enum, a struct and an array;
- add up totals by category and print them in the same order on every run;
- parse lines like
food 12.5and name the reason when a line is wrong; - see what you can already do in Swift and where to go next.

Picture an expense ledger. Two tabs stick out of its edge: food and taxi. You slip each receipt under the tab of its category. At the end of the month you add up the receipts under each tab. Then you draw a line at the bottom of the page and write the grand total: the sum line.
In this theme we'll build such a ledger in Swift. It needs no new constructs: the program is put together from what you already know. It grows from lesson to lesson, but the first code block of every lesson is a whole program you can run on its own.
Let's start with one receipt. You could store it as the string "food 12.5". But a string also accepts "fod 12.5" with a typo, and the compiler won't notice. And to add up the costs, you'd have to cut every string into pieces again.
It's better to describe a receipt with your own types, and then the compiler catches the typo. A model is the set of types a program uses to describe things from real life: here, a category and a receipt. Here is the ledger's model:
enum Category: String, CaseIterable {
case food, taxi
}
struct Expense: CustomStringConvertible {
let category: Category
let cost: Double
var description: String {
"\(category.rawValue) \(cost)"
}
}
let lunch = Expense(category: .food, cost: 12.5)
print(lunch) // food 12.5Let's take the program apart:
enum Categoryis the ledger's tabs. The list of categories is fixed in advance, and the program has no other category.struct Expenseis a receipt: a category and a cost under one name.- The
descriptionproperty says how a receipt looks when printed. Expense(category: .food, cost: 12.5)fills in a receipt with the memberwise initializer: the labels match the property names.print(lunch)takes the text fromdescriptionand printsfood 12.5.
Tabs: an enum
A category is a choice from a short list, so it is an enum. In the declaration enum Category: String, CaseIterable, a raw type and a protocol follow the colon:
Stringis the raw value type. A case without an explicit string gets its own name, so the raw value offoodis"food", the label on the tab.CaseIterableis a protocol that gives the array of all cases,allCases, in declaration order.
The raw type comes first, and protocols follow it after a comma. Swap them, enum Category: CaseIterable, String, and the program does not compile: raw type 'String' must appear first in the enum inheritance clause.
Let's see the raw values and allCases at work:
let labels = Category.allCases.map { $0.rawValue }
print(labels.joined(separator: " ")) // food taxi
if let tab = Category(rawValue: "taxi") {
print(tab) // taxi
}
print(Category(rawValue: "bus") == nil) // trueLet's walk through the program:
map { $0.rawValue }turns each tab into its label, andjoinedglues the labels together with a space:food taxi.Category(rawValue: "taxi")finds a tab by its label. The result is an optional, andif letopens it.- The ledger has no
bustab, soCategory(rawValue: "bus")givesnil.
A receipt: a struct
A receipt is data with two fields, so it is a struct. Its properties are declared with let: a written receipt is not corrected. The cost is a Double because it can have a fractional part: 12.5.
But a Double does not store every fraction exactly: print(0.1 + 0.2) prints 0.30000000000000004. It stores whole numbers, halves and quarters without loss, so the costs in this theme, 12.5, 7 and 4.25, add up exactly. Real apps often store money as a whole number of cents.
What else to know about Expense:
- Swift gave the struct its memberwise initializer, so we didn't have to write our own
init; - a number without a decimal point works too: in
Expense(category: .taxi, cost: 7)the property's type isDouble, so7is stored as7.0, the same as inlet price: Double = 5; descriptionsets the printed text; if you removeCustomStringConvertiblefrom the declaration,printshows a technical record likeExpense(category: main.Category.food, cost: 12.5). The program's name comes beforeCategory: afterswiftc main.swiftit ismain.
Interviewers like to ask this: why is the model a struct and not a class? The answer:
- a receipt is data, not a shared thing that several parts of the program change at once;
- a copy of a struct is independent: nobody changes your receipt behind your back;
- a receipt needs neither the
===check nor inheritance.
The ledger: an array of receipts
We put the receipts in an array, the stack in the ledger:
let expenses = [
lunch,
Expense(category: .taxi, cost: 7),
Expense(category: .food, cost: 4.25),
]
print(expenses.count) // 3
for expense in expenses {
print(expense)
}Swift infers the array's type from its elements: [Expense]. A comma after the last element is allowed, and it makes adding new receipts convenient. The loop prints three lines, each receipt through its description:
food 12.5taxi 7.0food 4.25
Here the ledger picture is inaccurate. In the ledger a receipt lies under its tab, while in the array all receipts sit in one stack, in the order they were added. The tab is only the category property of each receipt. To learn a tab's total, you'll have to go through the stack.
Common mistake
The category was passed as a string, like the label on a tab:
let ride = Expense(category: "taxi", cost: 7) // ✗error: cannot convert value of type 'String' to expected argument type 'Category'The program does not compile. The category property has the type Category, and "taxi" is a string, even though it matches the raw value. A string does not turn into a case by itself. Write the case, .taxi, or find it by its label with Category(rawValue: "taxi") and open the optional.
Key points
- A category is the enum
enum Category: String, CaseIterable: the raw type comes first, the protocol after a comma, andallCasesgives every case in order. - A receipt is the struct
Expensewith the constant propertiescategoryandcost;descriptionfromCustomStringConvertiblesets how it prints. - The receipts are kept in an
[Expense]array as one stack, in the order they were added. - A string does not become a case by itself:
Category(rawValue:)looks a case up by its label and returns an optional.