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.