1.5 — Collections, and the higher-order functions that came with them
Opening scenario
You open a code review and find this one-liner:
let names = users
.filter { $0.isActive }
.sorted { $0.createdAt > $1.createdAt }
.prefix(10)
.map(\.displayName)
Four operations, zero for loops, reads like a sentence. The first time you see it, it’s intimidating. The tenth time, it’s the only way you want to write Swift. This chapter gets you to the tenth time.
The three Swift collections you’ll use 99% of the time
| Type | What it is | When to reach for it |
|---|---|---|
Array<T> ([T]) | Ordered, indexable, allows duplicates | Default. Lists, sequences, anything ordered. |
Dictionary<K, V> ([K: V]) | Unordered key→value, keys must be Hashable | Lookups by id, configuration maps, counts. |
Set<T> | Unordered, unique, Hashable elements | Membership tests, deduplication. |
You’ll also occasionally touch Range (0..<10), ContiguousArray, OrderedDictionary (from swift-collections), but the three above carry most of daily life.
Concept → Why → How → Code
Concept: Swift collections are value types with copy-on-write
When you write let b = a for an array, Swift conceptually copies. But internally it shares the buffer until you mutate. The mutation triggers the actual copy. This is copy-on-write (COW). The upshot:
- You get value-type semantics (
bdoesn’t change whenadoes). - You don’t pay the copy cost unless you mutate.
- Passing an array to a function is cheap.
Why this matters
Other languages force you to choose: value semantics with copies (slow, safe) or reference semantics (fast, full of bugs). Swift gives you value semantics that are usually as cheap as references. You write naturally; the runtime optimizes.
How: the literal syntax
let xs: [Int] = [1, 2, 3]
let scores: [String: Int] = ["Ada": 95, "Linus": 88]
let tags: Set<String> = ["swift", "ios", "macos"]
let range = 0..<10 // half-open
let inclusive = 0...10 // closed
let empty1: [Int] = []
let empty2: [String: Int] = [:]
let empty3 = Set<String>()
Empty collection literals need a type annotation (Swift can’t infer []). Or use the explicit init.
Code: array essentials
var nums = [3, 1, 4, 1, 5, 9]
nums.append(2)
nums.insert(0, at: 0)
nums.remove(at: 2)
nums[1] = 99 // mutate by index
nums.count // 7
nums.isEmpty // false
nums.first // Int? — empty arrays return nil
nums.last // Int?
nums.contains(4) // Bool — O(n) for arrays
nums.indices // 0..<7 — for index-aware loops
Index out of bounds crashes. There is no automatic nil-return. Use nums.first, nums.last, or guard your indices.
Code: dictionary essentials
var ages = ["Ada": 36, "Linus": 54]
// Reading — subscript returns Int?
let adaAge = ages["Ada"] // Int? — nil if absent
// Reading with default
let unknown = ages["Bob", default: 0] // 0 (does not insert)
// Writing
ages["Grace"] = 87 // insert or overwrite
ages["Ada"] = nil // delete the key
ages.removeValue(forKey: "Linus") // alternative
// Iterating (order is not stable across runs)
for (name, age) in ages { print("\(name): \(age)") }
// Common: count occurrences
let words = "the the quick brown fox the lazy fox".split(separator: " ")
var counts: [Substring: Int] = [:]
for w in words { counts[w, default: 0] += 1 }
// counts == ["the": 3, "quick": 1, "brown": 1, "fox": 2, "lazy": 1]
The dict[key, default: …] subscript with += 1 is the canonical Swift counter pattern. Memorize it.
Code: set essentials
let a: Set = [1, 2, 3, 4]
let b: Set = [3, 4, 5, 6]
a.union(b) // {1, 2, 3, 4, 5, 6}
a.intersection(b) // {3, 4}
a.subtracting(b) // {1, 2}
a.isDisjoint(with: b) // false
a.contains(2) // O(1) — vs O(n) for array
When you find yourself checking array.contains(x) inside a loop, convert the array to a Set first. O(n²) → O(n).
Higher-order functions: the meat of the chapter
Every Swift collection type conforms to Sequence and Collection, which provide a rich set of methods that take closures. Master these:
let nums = [1, 2, 3, 4, 5]
// MAP — transform each element
let squared = nums.map { $0 * $0 }
// [1, 4, 9, 16, 25]
// FILTER — keep only matching elements
let even = nums.filter { $0.isMultiple(of: 2) }
// [2, 4]
// REDUCE — collapse to a single value
let total = nums.reduce(0, +) // 15
let product = nums.reduce(1, *) // 120
let csv = nums.reduce("") { $0 + "\($1)," } // "1,2,3,4,5,"
// COMPACTMAP — map + drop nils
let strings = ["1", "two", "3"]
let parsed = strings.compactMap { Int($0) }
// [1, 3]
// FLATMAP — map then flatten one level
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 }
// [1, 2, 3, 4]
// SORTED — returns a new sorted array
let mixed = [3, 1, 4, 1, 5, 9, 2, 6]
let asc = mixed.sorted() // ascending by default
let desc = mixed.sorted(by: >) // descending
let byCount = ["bb", "a", "ccc"].sorted { $0.count < $1.count }
// PREFIX / SUFFIX / DROPFIRST / DROPLAST — slicing
nums.prefix(3) // [1, 2, 3]
nums.suffix(2) // [4, 5]
nums.dropFirst() // [2, 3, 4, 5]
nums.dropLast(2) // [1, 2, 3]
// ALLSATISFY / CONTAINS / FIRST(WHERE:) — querying
nums.allSatisfy { $0 > 0 } // true
nums.contains { $0 > 4 } // true
nums.first { $0.isMultiple(of: 2) } // 2 (Int?)
// ENUMERATED — index + element pairs
for (i, n) in nums.enumerated() { print("\(i): \(n)") }
// ZIP — pairwise iteration over two sequences
for (name, age) in zip(["Ada", "Linus"], [36, 54]) {
print("\(name) is \(age)")
}
The KeyPath shorthand (Swift 5.2+) lets you replace { $0.title } with \.title:
let titles = articles.map(\.title) // instead of { $0.title }
let activeNames = users.filter(\.isActive).map(\.name)
This works wherever a (T) -> U is expected and U is a property of T.
In the wild
- JSON parsing pipelines: every
URLSession.dataTaskreturning JSON funnels through adecode → filter → map → sortchain. - SwiftUI’s
ForEach(items)iterates collections; idiomatic SwiftUI is full ofitems.filter { … }.sorted { … }to drive the view. - Core Data fetched results are converted to
[Entity]and then transformed with higher-order functions before display. - Networking layers convert
[APIPost]→[DomainPost]with.map(Post.init). This is called the mapper pattern and is everywhere in production iOS code.
Common misconceptions
-
“
mapandforloops are interchangeable; use whichever feels right.” Subtly wrong.mapreturns a new array of the same length. If you’re usingmapfor side effects (array.map { print($0) }), you’re misusing it. UseforEachor aforloop for side effects. The compiler will eventually warn you about the unused return value. -
“Higher-order functions are slow.” In Swift, the compiler aggressively inlines
map/filter/reduceclosures. The difference vs a hand-written loop is usually unmeasurable. Premature manual loops for “performance” is a 2014 attitude. -
“
Array.containsis fast.” O(n). For repeated lookups, convert to aSetonce and check membership in O(1). -
“
Dictionarypreserves insertion order.” Swift’sDictionarydoes not guarantee order. If you need ordered key-value pairs, useOrderedDictionaryfrom swift-collections. -
“
reduceis too clever for production.” Disagree, but the first parameter is the initial value, and the closure is(accumulator, element) -> accumulator. Once that clicks, it’s the most general tool in your kit.
Seasoned engineer’s take
A pipeline of higher-order functions is the declarative shape of a transformation. A for loop is the imperative shape. Both produce the same output; they have very different review and refactor costs.
- A
.filter { $0.isActive }.map(\.id)pipeline is self-documenting — a reader sees the intent (keep active users, take ids). - The equivalent
forloop with mutable accumulators requires the reader to execute the loop mentally to discover the same intent.
Use the pipeline form by default. Drop to a for loop when:
- You need early-exit (
break/return). - You’re producing multiple outputs from one pass (which would otherwise require iterating twice).
- The transformation involves more than ~3 steps; at that point break it into named functions with descriptive names — composition still beats a single megastatement, but legibility wins over chain length.
Also: be wary of flatMap on optional sequences. Modern Swift renamed the optional version to compactMap to avoid confusion. If you mean “map and drop nils”, use compactMap. If you mean “map and flatten nested arrays”, use flatMap.
TIP: The
lazymodifier (array.lazy.filter { … }.map { … }.first { … }) defers evaluation. Useful when you’re searching a huge collection and want to stop at the first match without materializing the intermediates.
WARNING:
SetandDictionaryiteration order is not guaranteed to be stable across runs (or even within a run, in theory). Never rely on the order. If your tests pass on macOS and fail in CI on Linux, this is often the cause.
Interview corner
Question: “Given an array of [User], return the top 5 active users by signup date, as [String] (their display names).”
Junior answer:
var actives: [User] = []
for u in users {
if u.isActive { actives.append(u) }
}
// then sort, then take 5, then map names…
→ Correct, verbose. They’ll ask: “can you do that in one line?”
Mid-level answer:
let result = users
.filter { $0.isActive }
.sorted { $0.createdAt > $1.createdAt }
.prefix(5)
.map { $0.displayName }
→ Strong. Pipeline is idiomatic.
Senior answer: All of the above, plus: “I’d reach for \.displayName keypath shorthand on the last map. I’d also point out this is O(n log n) because of the sort — fine for thousands, suboptimal for millions. For a very large input I’d use a min-heap of size 5 to do it in O(n log k). And if createdAt were nullable I’d handle the optional explicitly with compactMap rather than crash on a force-unwrap.”
let result = users
.filter(\.isActive)
.sorted { $0.createdAt > $1.createdAt }
.prefix(5)
.map(\.displayName)
→ Senior signal. Knows the language idioms, the complexity, and the production edge cases.
Red-flag answer: “I’d write a custom sort algorithm because the built-in one isn’t tuned for my data.” → Unless you’ve benchmarked, this is a make-work answer. Swift’s sort is Timsort-style; it’s excellent.
Lab preview
Lab 1.A (Playground exploration) includes a section where you’ll process a sample dataset (the words of Hamlet) using only higher-order functions: word counts, longest words by length, top-N alphabetized. No for loops allowed.
Next: how Swift models kinds of things — structs, classes, enums, protocols, and the religious war between them. → Structs, classes, enums, protocols