Skip to content
Theory · Final project · lesson 1 of 5

The model: expenses

What you'll learn

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.5 and name the reason when a line is wrong;
  • see what you can already do in Swift and where to go next.
An open expense ledger with tabs food and taxi taking in receipts, and a total line sum

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:

SWIFT
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.5

Let's take the program apart:

  1. enum Category is the ledger's tabs. The list of categories is fixed in advance, and the program has no other category.
  2. struct Expense is a receipt: a category and a cost under one name.
  3. The description property says how a receipt looks when printed.
  4. Expense(category: .food, cost: 12.5) fills in a receipt with the memberwise initializer: the labels match the property names.
  5. print(lunch) takes the text from description and prints food 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:

  • String is the raw value type. A case without an explicit string gets its own name, so the raw value of food is "food", the label on the tab.
  • CaseIterable is 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:

SWIFT
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) // true

Let's walk through the program:

  1. map { $0.rawValue } turns each tab into its label, and joined glues the labels together with a space: food taxi.
  2. Category(rawValue: "taxi") finds a tab by its label. The result is an optional, and if let opens it.
  3. The ledger has no bus tab, so Category(rawValue: "bus") gives nil.

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 is Double, so 7 is stored as 7.0, the same as in let price: Double = 5;
  • description sets the printed text; if you remove CustomStringConvertible from the declaration, print shows a technical record like Expense(category: main.Category.food, cost: 12.5). The program's name comes before Category: after swiftc main.swift it is main.

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:

SWIFT
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.5
  • taxi 7.0
  • food 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:

SWIFT
let ride = Expense(category: "taxi", cost: 7) // ✗
TEXT
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, and allCases gives every case in order.
  • A receipt is the struct Expense with the constant properties category and cost; description from CustomStringConvertible sets 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.