Collections

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Swift ships three collection types you reach for constantly — Array, Set and Dictionary — built on a protocol hierarchy that lets a single set of higher-order operations (map, filter, reduce, and the rest) work identically over all of them, over ranges, and over any type you write yourself.

Array

var shoppingList = ["Eggs", "Milk"]                 // type inferred as [String]
let emptyArray = [Int]()                             // explicit element type, empty
var threeDoubles = Array(repeating: 0.0, count: 3)   // [0.0, 0.0, 0.0]

shoppingList.append("Flour")                          // mutation requires `var`
shoppingList += ["Baking Powder"]
shoppingList.insert("Maple Syrup", at: 0)
shoppingList[1...3] = ["Bananas", "Apples"]            // range subscript replaces a run of elements

print(shoppingList.first ?? "empty", shoppingList.last ?? "empty", shoppingList.count, shoppingList.isEmpty)

let mapleSyrup = shoppingList.remove(at: 0)
for (index, item) in shoppingList.enumerated() {
    print("Item \(index + 1): \(item)")
}

Assigning an Array to a new let/var, or passing it to a function, copies it — as with String and every other Swift collection, that copy is only actually materialized, copy-on-write, the moment either side is mutated.

Set

var letters = Set<Character>()                        // an empty set of Characters
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]   // set literal, type inferred

favoriteGenres.insert("Jazz")
if let removed = favoriteGenres.remove("Rock") { print("\(removed)? Never much cared for it.") }
print(favoriteGenres.contains("Classical"))

let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimes: Set = [2, 3, 5, 7]

print(oddDigits.union(evenDigits).sorted())                    // all ten digits
print(oddDigits.intersection(evenDigits).sorted())              // []
print(oddDigits.subtracting(singleDigitPrimes).sorted())        // [1, 9]
print(oddDigits.symmetricDifference(singleDigitPrimes).sorted()) // [1, 2, 9]

Every Set element must conform to Hashable, providing a hashValue-backed == so membership and set-algebra operations run in roughly constant time instead of scanning; unlike Array, a Set has no defined order, which is exactly why it offers union/intersection/subtraction/symmetric-difference instead of index-based mutation.

Dictionary

var namesOfIntegers: [Int: String] = [:]              // empty dictionary literal
namesOfIntegers[16] = "sixteen"

var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
airports["LHR"] = "London Heathrow"
airports["LHR"] = "London Heathrow Airport"            // updates the existing value
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
    print("The old value for DUB was \(oldValue).")
}

if let airportName = airports["DUB"] {                  // subscript access returns an Optional
    print("The name of the airport is \(airportName).")
}
airports["APL"] = nil                                    // assigning nil removes a key-value pair

for (airportCode, airportName) in airports {             // iteration yields (key, value) tuples, order not guaranteed
    print("\(airportCode): \(airportName)")
}
print(Array(airports.keys), Array(airports.values))

Mutability, ArraySlice, and Ranges as Collections

let fibonacci = [1, 1, 2, 3, 5, 8, 13]
let middle: ArraySlice<Int> = fibonacci[2...4]          // [2, 3, 5] -- shares fibonacci's storage, no copy yet
print(middle.startIndex, middle.endIndex)                 // 2 5 -- indices are preserved from the original array

let backToArray = Array(middle)                           // convert once you plan to keep the slice around

print((1...5).map { $0 * $0 })                              // ClosedRange is itself a Collection
print((0..<3).reduce(0, +))                                  // 3 -- so is Range

let/var govern collection mutability exactly as with String: a let array, set, or dictionary cannot be mutated at all, even element-by-element. ArraySlice<Element> is Array’s counterpart to `Substring — a non-owning, storage-sharing view meant for short-term use — and both Range<Bound> and ClosedRange<Bound> conform to Collection (when Bound is a Strideable integer type), so the range operators from Operators are themselves usable with every sequence algorithm on this page.

The Sequence/Collection Protocol Family

The Swift collection protocol hierarchy: Sequence at the root, refined by Collection, refined by BidirectionalCollection, refined in turn by RandomAccessCollection, MutableCollection and RangeReplaceableCollection, with Array, Set, Dictionary, Range and ArraySlice placed at the protocols they conform to

Each protocol in the chain adds one capability, and conforming types pick up every algorithm written against the protocols they satisfy for free:

  • Sequence — a single-pass walk (for-in, makeIterator()); no guarantee it can be walked twice.

  • Collection — refines Sequence with a stable startIndex/endIndex, subscript access by index, and multi-pass iteration.

  • BidirectionalCollection — adds index(before:), so it can be walked backwards.

  • RandomAccessCollection — adds O(1) index arithmetic (Array conforms; a linked-list-backed collection typically would not).

  • MutableCollection — adds a settable subscript, for in-place element replacement without changing length.

  • RangeReplaceableCollection — adds insert/remove/append over arbitrary subranges, changing length.

Array conforms to all six; Set and Dictionary conform only through Collection (they have no meaningful before/after order, so neither is BidirectionalCollection nor RandomAccessCollection, and neither supports arbitrary subrange replacement).

Higher-Order Functions

let numbers = [1, 2, 3, 4, 5, 6, 7, 8]

let doubled = numbers.map { $0 * 2 }                         // [2, 4, 6, 8, 10, 12, 14, 16]
let evens = numbers.filter { $0 % 2 == 0 }                    // [2, 4, 6, 8]
let sum = numbers.reduce(0) { $0 + $1 }                        // 36
let sumShorthand = numbers.reduce(0, +)                         // 36, using an operator as a function value

let strings = ["3", "banana", "-7", "42"]
let parsed = strings.compactMap { Int($0) }                    // [3, -7, 42] -- drops the elements that fail
let nested = [[1, 2], [3, 4], [5]]
let flattened = nested.flatMap { $0 }                            // [1, 2, 3, 4, 5]

let sortedDescending = numbers.sorted(by: >)                     // [8, 7, 6, 5, 4, 3, 2, 1]
let firstEven = numbers.first(where: { $0.isMultiple(of: 2) })   // Optional(2)
print(numbers.contains(where: { $0 > 6 }))                        // true

lazy, zip, enumerated, and stride

let names = ["Anna", "Alex", "Brian", "Jack"]
let ages = [61, 32, 25, 43]

for (name, age) in zip(names, ages) {                    // pairs elements until the shorter sequence runs out
    print("\(name) is \(age)")
}

for (index, name) in names.enumerated() {
    print("\(index): \(name)")
}

for value in stride(from: 0, to: 10, by: 2) { print(value) }     // 0 2 4 6 8 -- half-open, like ..<
for value in stride(from: 1, through: 10, by: 3) { print(value) } // 1 4 7 10 -- closed, like ...

let hugeRange = 1...1_000_000
let firstFiveSquares = hugeRange.lazy
    .map { $0 * $0 }
    .filter { $0 % 2 == 0 }
    .prefix(5)                                              // computed on demand, element by element
print(Array(firstFiveSquares))

Without .lazy, chaining map/filter over a huge sequence builds a complete intermediate array at every step; .lazy defers each transformation until an element is actually consumed, which is what makes .prefix(5) above touch only as many elements of hugeRange as it needs.

InlineArray and Span (Swift 6.2)

// InlineArray<count, Element>: a fixed-size, inline-storage array -- no heap allocation, no copy-on-write
// indirection -- useful where a small, statically-sized buffer matters, such as tight numeric code.
let fixed: InlineArray<4, Int> = [1, 2, 3, 4]
print(fixed[0], fixed.count)

// Span<Element>: a non-owning, safe view over contiguous memory -- a memory-safe alternative to
// UnsafeBufferPointer for reading contiguous storage without copying it or taking ownership.
func sum(of values: Span<Int>) -> Int {
    var total = 0
    for i in values.indices { total += values[i] }
    return total
}

Both types arrived in the Swift 6.2 standard library as part of an ongoing push for allocation-free, memory-safe alternatives to heap-backed collections and unsafe pointers; Span is covered again alongside its unsafe-pointer relatives in Memory Safety and Unsafe Pointers.

See Also

  • Strings and Characters — String, which shares this page’s Collection conformance style.

  • Control Flow — for-in over the collections introduced here.

  • Functional Programming — map/filter/reduce and friends, examined as a style rather than a per-type API.

  • Generics — writing your own Sequence/Collection conformances and algorithms generic over them.

  • Standard Library Overview — where these protocols and types sit among the rest of the standard library.