Collections

Swift's standard library provides three core collections — Array, Dictionary, and Set. All three are value types with copy-on-write storage, so they behave like independent copies while still sharing memory until one of them changes.

This lesson covers how to build and query each collection, how to iterate them with higher-order functions, and what value semantics mean in practice.

Arrays

An Array is an ordered, index-addressed list. Element order is preserved and duplicates are allowed, which makes it the default choice for most sequences of data.

Creating Arrays

Array literals use square brackets. The element type is inferred from the contents; an empty array needs an annotation because there is nothing to infer from.

let primes = [2, 3, 5, 7]                   // [Int] — inferred
let names = ["Ada", "Alan"]                 // [String]
let zeros = Array(repeating: 0, count: 3)   // [0, 0, 0]
let empty: [Double] = []                    // empty array needs a type

print(primes.count, names.first ?? "")      // prints: 4 Ada
print(zeros, empty.count)                   // prints: [0, 0, 0] 0

Accessing Elements

Use a subscript for direct access, and the first / last properties when the array might be empty, since those return optionals instead of trapping.

let primes = [2, 3, 5, 7]

print(primes[0])                     // subscript: first element → 2
print(primes[primes.count - 1])      // last element via computed index → 7
print(primes.first ?? -1)            // optional access: -1 when empty
print(primes.last ?? -1)             // optional access: -1 when empty

// print(primes[99])                 // ❌ fatal error: Index out of range
print(primes.indices.contains(99))   // prints: false — test before indexing

Adding & Removing

Mutating methods require a var array. Each operation says whether it returns the removed element so you can capture it.

var cart = ["apple", "pear"]
cart.append("fig")               // add to the end
cart.insert("date", at: 1)       // insert at an index

let removed = cart.removeLast()  // pop and return the last element
print(cart, removed)             // prints: ["apple", "date", "pear"] fig

cart.remove(at: 0)               // remove by position
cart += ["kiwi"]                 // += appends a whole sequence
print(cart)                      // prints: ["date", "pear", "kiwi"]

Dictionaries

A Dictionary maps unique keys to values. Lookup is fast, but the order of the pairs is not defined, so never rely on it for output.

Creating Dictionaries

A dictionary literal uses key: value pairs in square brackets. The empty initializer form requires explicit key and value types.

let ages = ["Ada": 36, "Alan": 41]        // [String: Int] — inferred
let counters = [String: Int]()            // empty dictionary with a type

print(ages.count, counters.isEmpty)       // prints: 2 true

Access & Defaults

Subscripting with a key returns an optional, because the key may be missing. When a fallback is acceptable, the default: subscript returns a plain value instead.

var stock = ["pen": 10, "book": 3]

print(stock["pen"] ?? 0)             // Optional → 10
print(stock["lamp", default: 0])     // default subscript → 0, no Optional

stock["pen"] = 12                    // update an existing key
stock["ruler"] = 5                   // insert a brand-new key
print(stock.count)                   // prints: 3

Iterating

Iterating yields (key, value) tuples. Because the order is unstable, sort the keys first whenever output must be reproducible.

let ages = ["Ada": 36, "Alan": 41]

for (name, age) in ages {            // order is NOT guaranteed
    print(name, age)
}

for key in ages.keys.sorted() {      // sorted → stable output
    print(key, ages[key] ?? 0)       // prints: Ada 36, then Alan 41
}

Sets

A Set stores unique, unordered elements and answers "is this present?" in constant time on average. Reach for a set when membership matters more than order, or when you need to remove duplicates.

Creating Sets

The element type must be hashable. An explicit annotation is required, because a bare literal would otherwise be read as an array.

let vowels: Set<Character> = ["a", "e", "i", "o", "u"]   // explicit type
let fromArray = Set([1, 2, 2, 3, 3, 3])                 // duplicates collapse

print(vowels.count, fromArray.count)                    // prints: 5 3
print(fromArray)                                        // order is not defined

Set Operations

Sets model the mathematical operations you would expect, and every result is itself a set — sort it when you need a stable print order.

let a: Set = [1, 2, 3, 4]
let b: Set = [3, 4, 5, 6]

print(a.union(b).sorted())                 // [1, 2, 3, 4, 5, 6]
print(a.intersection(b).sorted())          // [3, 4]
print(a.subtracting(b).sorted())           // [1, 2]
print(a.symmetricDifference(b).sorted())   // [1, 2, 5, 6]

Membership Tests

contains is the set's headline feature. Subset, superset, and disjoint tests round out the vocabulary.

let allowed: Set = ["read", "write"]

print(allowed.contains("read"))                          // prints: true
print(allowed.isSubset(of: ["read", "write", "exec"]))   // prints: true
print(allowed.isDisjoint(with: ["exec"]))                // prints: true

Iteration & Higher-Order Functions

All three collections conform to Sequence, so the same loop syntax and the same functional toolkit work on each of them.

Iterating Collections

A plain for … in walks the elements; enumerated() adds a zero-based position when you need the index as well.

let scores = [88, 92, 75]

for score in scores {                      // element only
    print(score)
}

for (index, score) in scores.enumerated() { // position + element
    print(index, score)                     // prints: 0 88, 1 92, 2 75
}

map, filter, reduce

These three cover most data shaping: map transforms every element, filter keeps the ones that match, and reduce folds the whole sequence into one value.

let scores = [88, 92, 75]

print(scores.map { $0 + 5 })             // [93, 97, 80] — transform each
print(scores.filter { $0 >= 90 })        // [92] — keep matching elements
print(scores.reduce(0, +))               // 255 — sum via the + operator

Chaining & Laziness

Because each function returns a new sequence, they chain naturally. Prefix the chain with lazy to compute on demand instead of building intermediate arrays.

let words = ["swift", "is", "fast"]

let result = words
    .filter { $0.count > 2 }      // keeps "swift" and "fast"
    .map { $0.uppercased() }      // transforms each remaining element
    .joined(separator: " ")
print(result)                     // prints: SWIFT FAST

// lazy defers the work: nothing runs until the final result is requested
let lazyCount = words.lazy.filter { $0.count > 2 }.count
print(lazyCount)                  // prints: 2

Value Semantics

Assigning a collection copies its value, not a reference to shared mutable state. This is the property that makes Swift collections safe to pass around without defensive copying.

Copy on Assignment

Each variable owns its own logical value, so mutating one never surprises another — no aliasing bugs.

var first = [1, 2, 3]
var second = first               // a copy, logically
second.append(4)                 // mutate the copy only

print(first)                     // prints: [1, 2, 3]
print(second)                    // prints: [1, 2, 3, 4]

Copy-on-Write

The copy is deferred for performance: two variables share one storage buffer until one of them writes. The write then duplicates the buffer so the other value stays intact.

var a = Array(repeating: 0, count: 1_000_000)   // one large buffer
var b = a            // COW: no duplicate allocation yet — both share it
b[0] = 99            // this first write triggers the copy, then mutates b

print(a[0], b[0])    // prints: 0 99 — a's buffer was never touched
print(a.count)       // prints: 1000000

Common Pitfalls

Out-of-Range Indexing

Array subscripts trap on an invalid index, so test the position first or use the optional first / last properties.

var list = [10, 20, 30]
list.removeLast()                // list is now [10, 20]

// print(list[2])                // ❌ fatal error: index out of range
print(list.indices.contains(2))  // prints: false — safe to check
print(list.last ?? -1)           // prints: 20 — optional access

Mutating While Iterating

Changing a collection's size inside the loop that walks it invalidates the iteration. Build a new collection instead, then replace the original if needed.

var nums = [1, 2, 3, 4]
var kept: [Int] = []             // collect results in a NEW array

for n in nums {
    if n % 2 == 0 { kept.append(n) }   // never mutate `nums` here
}
print(kept)                      // prints: [2, 4]

That completes the foundations: syntax, values, types, text, and collections. Next phase moves into Expressions & Operators and real program logic.