Section 1 · Swift the Language
Collections and Control Flow
17 min4 min readChoose wisely
Lesson at a glance
Store expenses in arrays, dictionaries and sets, loop over them with for-in, and branch with an exhaustive switch that the compiler checks for you.
- 1Arrays: ordered, indexed, typed
- 2Dictionaries: lookups that might miss
- 3Sets: unique values
- 4Choosing a collection
- 5switch: exhaustive by design
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
One expense is a value. A month of expenses is a collection. Pocket Budget needs three shapes of collection: an ordered list of amounts, a lookup from category to monthly limit, and a bag of unique tags. Swift gives you exactly those three, and all of them are typed, so an array of amounts can never accidentally contain a String.
Arrays: ordered, indexed, typed
import Foundation
var amounts: [Decimal] = [12.5, 4.25, 60]
amounts.append(18)
print(amounts.count) // 4
print(amounts[0]) // 12.5
print(amounts.first as Any) // Optional(12.5)[Decimal] reads “array of Decimal”. Indexes start at 0. Notice the difference between amounts[0] and amounts.first: the subscript trusts you and crashes with “Index out of range” on an empty array, while first returns an optional and lets you handle the empty case with the tools from the previous lesson.
Most of the time you don’t need indexes at all. A for-in loop visits every element:
var total: Decimal = 0
for amount in amounts {
total += amount
}
print(total) // 94.75
for (index, amount) in amounts.enumerated() {
print("\(index + 1). \(amount)")
}enumerated() hands you pairs of position and value, which is the safe way to number rows. Ranges work with for-in too, in two flavours:
for day in 1...30 { } // closed range: 1 through 30
for i in 0..<amounts.count { } // half-open: stops before countThe closed range includes its upper bound; the half-open range stops one short, which is exactly what array indexes need.
Dictionaries: lookups that might miss
A dictionary maps keys to values. Pocket Budget keeps a monthly limit per category:
let limits: [String: Decimal] = ["Food": 300, "Transport": 120]
var spent: [String: Decimal] = ["Food": 280]
let foodLimit = limits["Food"] // Decimal?, Optional(300)
let funLimit = limits["Fun"] ?? 0 // Decimal, 0
spent["Fun", default: 0] += 15 // creates "Fun": 15Every lookup returns an optional, because the key might not be there. That’s the same safety idea again, now built into a collection. The default: subscript is the idiomatic way to accumulate totals: it reads the existing value or starts from your default, then writes the result back.
Sets: unique values
var tags: Set<String> = ["work", "travel"]
tags.insert("work") // already there, nothing changes
print(tags.count) // 2
print(tags.contains("travel")) // trueA set has no order and no duplicates, and contains stays fast no matter how many items it holds. Use it for tags, selected filters and “have I seen this id already?” checks.
Choosing a collection
| You need | Use | Pocket Budget example |
|---|---|---|
| Order, duplicates allowed, access by position | Array | This month’s expenses, newest first |
| Look up a value by a key | Dictionary | Monthly limit for each category |
| Membership and uniqueness, order irrelevant | Set | Tags on one expense |
When in doubt, start with an array. It’s the collection SwiftUI lists display, and it’s the easiest to read in a debugger. Switch to a dictionary when you catch yourself searching an array for “the one with this name”, and to a set when you catch yourself checking for duplicates before every append.
All three collections are values, not shared objects. var copy = amounts gives you an independent array: appending to copy leaves amounts untouched. That behaviour has a name, value semantics, and it gets its own lesson in the next section because it shapes how you design every model type.
switch: exhaustive by design
switch in Swift is much more than a chain of equality checks. It matches ranges, binds values and adds conditions, and the compiler insists that every possible value is handled. Here is how Pocket Budget picks a status message from the percentage of a budget used:
func status(percentUsed: Int) -> String {
switch percentUsed {
case ..<0:
return "Check your numbers"
case 0..<50:
return "Plenty left"
case 50..<90:
return "On track"
case 90...100:
return "Nearly at your limit"
default:
return "Over budget"
}
}
print(status(percentUsed: 95)) // Nearly at your limitTwo things differ from C-family languages. First, there is no break and no accidental fall-through: once a case matches, the switch is done. Second, remove the default and the build fails with “switch must be exhaustive”, because an Int has values your cases don’t cover. That error looks annoying with integers. With enums, in the next section, it becomes the most useful error in the language: add a new case to your model and the compiler lists every switch you forgot to update.
Cases can also bind the matched value and test it with where:
let used: Decimal = 15
let limit: Decimal = 10
switch used {
case 0:
print("Nothing spent yet")
case let amount where amount > limit:
print("Over by \(amount - limit)")
default:
print("Within budget")
}Swift 5.9 also made switch and if usable as expressions, so let label = switch percentUsed { … } works when every case produces a single value. Use whichever form reads better; the exhaustiveness rule is the same.
You can now hold a month of data and make decisions about it. The loops in this lesson work, but they’re longhand. Next you’ll package logic into functions and replace most of those loops with closures like filter, map and reduce.
Key takeaways
- An array is ordered and indexed from 0; reading past the end crashes, so prefer
first,lastandfor-inover manual indexes. - [object Object]
- A set stores unique values with fast
containschecks and no order. - A Swift
switchmust cover every possible value, has no implicit fallthrough, and can match ranges andwhereconditions.