Data structures
Swift’s principal data structures are value-typed and generic: Array<T> (ordered, indexed), Dictionary<K, V> (hash map), Set<T> (unordered unique), Range<T> (numeric or character spans), tuples (anonymous heterogeneous), and the Optional<T> enum. Structs and enums admit user-defined types; enums admit associated values (sum types — substantial discrimination capability) and raw values (mapping to underlying primitives). The standard library provides substantial protocol-oriented surfaces — Sequence, Collection, Hashable, Codable, Comparable — admitting substantial functionality through conformance. The combination — value-typed collections with COW, generic types, enums with associated values, the Codable protocol for serialisation — is the substance of Swift’s data-structure surface.
Arrays
Ordered, indexed, dynamically-sized:
var arr: [Int] = [1, 2, 3]
var arr = [Int]() // empty
var arr = Array<Int>() // verbose form
var arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
var arr = Array(0..<10) // [0, 1, ..., 9]
Access
arr.count // length
arr.isEmpty // bool
arr[0] // 1 (crashes on out-of-bounds)
arr.first // Optional(1)
arr.last // Optional(3)
arr.first(where: { $0 > 1 }) // first matching
// Slicing:
arr[0..<2] // ArraySlice
arr[1...]
arr[..<2]
let copy = Array(arr[1...]) // independent Array
// Reverse:
arr.reversed()
Mutation
arr.append(4) // adds at end
arr.append(contentsOf: [5, 6, 7])
arr += [8, 9] // operator form
arr.insert(0, at: 0) // insert at index
arr.remove(at: 0) // remove at index
arr.removeFirst()
arr.removeLast()
arr.removeAll(where: { $0 < 0 })
arr[2] = 99 // update index
arr.sort() // mutating sort
arr.reverse()
arr.shuffle()
Iteration
for x in arr {
print(x)
}
for (i, x) in arr.enumerated() {
print("[\(i)] = \(x)")
}
// Functional:
let doubled = arr.map { $0 * 2 }
let evens = arr.filter { $0.isMultiple(of: 2) }
let sum = arr.reduce(0, +)
Treated in Loops.
Array properties
Arrays are value types — assignment copies (with COW):
var a = [1, 2, 3]
var b = a // copy
b.append(4)
print(a) // [1, 2, 3] (unchanged)
print(b) // [1, 2, 3, 4]
For contiguous storage (no Objective-C bridging), ContiguousArray<T>:
var arr: ContiguousArray<Int> = [1, 2, 3]
Conventional only when bridging to Objective-C is undesirable.
Dictionaries
Hash maps:
var scores: [String: Int] = ["Alice": 95, "Bob": 87]
var scores = ["Alice": 95, "Bob": 87] // type inferred
var scores = [String: Int]() // empty
Access
scores["Alice"] // Optional(95)
scores["Charlie"] // nil
// Default:
scores["Alice"] ?? 0
scores["Charlie", default: 0] // 0 (no nil)
scores.count
scores.isEmpty
scores.keys // Dictionary<K, V>.Keys
scores.values
Mutation
scores["Charlie"] = 78 // add or update
scores.updateValue(100, forKey: "Alice") // returns old value
scores["Alice"] = nil // remove
scores.removeValue(forKey: "Alice") // returns the removed value
scores.merge(["Dave": 88]) { current, _ in current } // merge with conflict resolution
Iteration
for (key, value) in scores { // unordered
print("\(key): \(value)")
}
for key in scores.keys {
print(key)
}
// Sorted:
for (key, value) in scores.sorted(by: { $0.key < $1.key }) {
print("\(key): \(value)")
}
Dictionaries are value types; iteration order is unspecified (consistent within a run; differs across runs).
Default values
var counts = [String: Int]()
for word in words {
counts[word, default: 0] += 1
}
The [key, default: x] admits substantial conciseness for “increment-or-initialise”.
Sets
Unordered unique collections:
var fruits: Set<String> = ["apple", "banana", "apple"]
print(fruits) // ["banana", "apple"] (unique)
fruits.insert("cherry") // returns (inserted: Bool, memberAfterInsert: T)
fruits.contains("apple")
fruits.remove("banana")
let a: Set = [1, 2, 3]
let b: Set = [2, 3, 4]
a.union(b) // {1, 2, 3, 4}
a.intersection(b) // {2, 3}
a.subtracting(b) // {1}
a.symmetricDifference(b) // {1, 4}
a.isSubset(of: [1, 2, 3, 4, 5]) // true
a.isSuperset(of: [1, 2])
a.isStrictSubset(of: [1, 2, 3, 4]) // true (subset and not equal)
a.isDisjoint(with: [10, 20]) // true
The element type must conform to Hashable.
Tuples
Anonymous fixed-size aggregates:
let pair: (String, Int) = ("Alice", 30)
let triple: (Int, Int, String) = (1, 2, "three")
// Destructuring:
let (name, age) = pair
print(name, age) // "Alice 30"
// Named elements:
let person: (name: String, age: Int) = (name: "Alice", age: 30)
print(person.name, person.age)
// By index:
print(pair.0, pair.1)
Tuples admit substantial conciseness for multi-return values:
func divmod(_ a: Int, _ b: Int) -> (quotient: Int, remainder: Int) {
(a / b, a % b)
}
let result = divmod(17, 5)
print(result.quotient, result.remainder) // 3 2
let (q, r) = divmod(17, 5)
Tuples conform to Equatable if their components do.
Structs
User-defined value types:
struct Point {
var x: Double
var y: Double
func distanceTo(_ other: Point) -> Double {
sqrt((x - other.x).squared + (y - other.y).squared)
}
mutating func translate(by delta: Point) { // mutating required
x += delta.x
y += delta.y
}
}
var p = Point(x: 1, y: 2)
p.translate(by: Point(x: 10, y: 20))
Structs admit synthesised initialisers:
struct Person {
let name: String
let age: Int
}
// Compiler synthesises:
// init(name: String, age: Int)
let p = Person(name: "Alice", age: 30)
Treated in Value and reference types.
Enums
User-defined sum types:
enum Direction {
case up
case down
case left
case right
}
let d: Direction = .up
Raw values
Enums admit raw values:
enum Status: String {
case active = "active"
case inactive = "inactive"
case banned = "banned"
}
let s: Status = .active
print(s.rawValue) // "active"
let parsed = Status(rawValue: "active") // Optional<.active>
enum Priority: Int {
case low = 1
case medium = 2
case high = 3
}
print(Priority.high.rawValue) // 3
let p = Priority(rawValue: 2) // Optional<.medium>
Common raw types: String, Int, Double. The compiler synthesises init(rawValue:).
Associated values
Enums admit associated values — distinguishing them from C-style enums:
enum Result<Success, Failure: Error> {
case success(Success)
case failure(Failure)
}
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case triangle(base: Double, height: Double)
}
let s: Shape = .circle(radius: 5)
let r: Result<Int, MyError> = .success(42)
The associated values admit substantial discrimination via pattern matching:
switch s {
case .circle(let r):
print("circle radius \(r)")
case .rectangle(let w, let h):
print("rectangle \(w) x \(h)")
case .triangle(let b, let h):
print("triangle base \(b), height \(h)")
}
Treated in Pattern matching.
Methods on enums
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
func area() -> Double {
switch self {
case .circle(let r): return Double.pi * r * r
case .rectangle(let w, let h): return w * h
}
}
}
Recursive enums
The indirect admits recursion:
indirect enum Expr {
case literal(Int)
case add(Expr, Expr)
case mul(Expr, Expr)
}
let e = Expr.add(.literal(2), .mul(.literal(3), .literal(4)))
The mechanism admits substantial AST-style data structures.
CaseIterable
enum Direction: CaseIterable {
case up, down, left, right
}
for d in Direction.allCases {
print(d)
}
Range
Spans of values:
let r1 = 0..<10 // half-open
let r2 = 0...10 // closed
let r3 = "a"..."z" // character range
let oneSided1 = 5... // [5, 6, ..., infinity)
let oneSided2 = ..<10 // (-infinity, 10)
let oneSided3 = 0... // [0, infinity)
// Ranges as sequences:
for n in 0..<5 {
print(n) // 0, 1, 2, 3, 4
}
// Range methods:
r1.contains(5) // true
r1.count // 10
r1.lowerBound // 0
r1.upperBound // 10
Ranges are conformant Sequence; iteration admits substantial efficiency.
Codable
The Codable protocol admits automatic serialisation/deserialisation:
struct User: Codable {
let id: Int
let name: String
let email: String
}
// Encode:
let user = User(id: 1, name: "Alice", email: "a@b.c")
let json = try JSONEncoder().encode(user)
print(String(data: json, encoding: .utf8)!)
// {"id":1,"name":"Alice","email":"a@b.c"}
// Decode:
let decoded = try JSONDecoder().decode(User.self, from: json)
The conformance is synthesised automatically for structs/classes/enums whose properties are all Codable. The standard types (String, Int, Double, Date, URL, Data, Array, Dictionary, Set, Optional) conform.
Custom encoding
struct User: Codable {
let id: Int
let name: String
let email: String
enum CodingKeys: String, CodingKey {
case id
case name = "user_name" // map JSON "user_name" to name
case email
}
}
The CodingKeys admits renaming and excluding fields.
For substantial customisation:
struct User: Codable {
let id: Int
let createdAt: Date
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let timestamp = try container.decode(Double.self, forKey: .createdAt)
createdAt = Date(timeIntervalSince1970: timestamp)
}
enum CodingKeys: String, CodingKey {
case id, createdAt
}
}
Common patterns
Array operations
let arr = [1, 2, 3, 4, 5]
let doubled = arr.map { $0 * 2 }
let evens = arr.filter { $0.isMultiple(of: 2) }
let sum = arr.reduce(0, +)
let max = arr.max() ?? 0
let sorted = arr.sorted(by: >)
let unique = Array(Set(arr)) // dedup (loses order)
Dictionary operations
let scores = ["Alice": 95, "Bob": 87]
scores.mapValues { $0 + 5 } // [String: Int]
scores.filter { $0.value > 90 }
scores.compactMapValues { $0 > 0 ? $0 : nil } // remove non-positive
let sortedByValue = scores.sorted { $0.value > $1.value }
Group-by
let grouped = Dictionary(grouping: people) { $0.age / 10 * 10 }
// [decade: [people]]
Tally
var counts: [String: Int] = [:]
for word in words {
counts[word, default: 0] += 1
}
// Or with reduce:
let counts2 = words.reduce(into: [String: Int]()) { acc, word in
acc[word, default: 0] += 1
}
Set operations
let required: Set = ["name", "email", "password"]
let provided: Set = ["name", "email"]
let missing = required.subtracting(provided) // {"password"}
let extra = provided.subtracting(required)
Type-parameterised stack
struct Stack<Element> {
private var items: [Element] = []
var isEmpty: Bool { items.isEmpty }
var top: Element? { items.last }
mutating func push(_ x: Element) {
items.append(x)
}
mutating func pop() -> Element? {
items.popLast()
}
}
var s = Stack<Int>()
s.push(1)
s.push(2)
s.pop() // Optional(2)
Codable with nested types
struct Order: Codable {
let id: Int
let items: [Item]
let customer: Customer
struct Item: Codable {
let name: String
let quantity: Int
let price: Double
}
struct Customer: Codable {
let id: Int
let name: String
}
}
let json = try JSONEncoder().encode(order)
let decoded = try JSONDecoder().decode(Order.self, from: json)
Pretty-printed JSON
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let json = try encoder.encode(user)
Date encoding
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
Snake-case conversion
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
The conventions admit substantial handling of API conventions.
Enum with raw values for serialisation
enum Status: String, Codable {
case active
case inactive
case banned
}
struct User: Codable {
let name: String
let status: Status // Codable through raw value
}
Discriminated union via enum
enum LoadState<T: Codable>: Codable {
case idle
case loading
case loaded(T)
case failed(message: String)
enum CodingKeys: String, CodingKey {
case state
case data
case message
}
enum State: String, Codable {
case idle, loading, loaded, failed
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .idle:
try container.encode(State.idle, forKey: .state)
case .loading:
try container.encode(State.loading, forKey: .state)
case .loaded(let value):
try container.encode(State.loaded, forKey: .state)
try container.encode(value, forKey: .data)
case .failed(let message):
try container.encode(State.failed, forKey: .state)
try container.encode(message, forKey: .message)
}
}
}
Result for async outcomes
let result: Result<Data, Error>
switch result {
case .success(let data):
process(data)
case .failure(let error):
log(error)
}
Range for indexing
let arr = [10, 20, 30, 40, 50]
let first3 = arr[0..<3] // [10, 20, 30]
let last2 = arr[3...] // [40, 50]
let middle = arr[1..<4] // [20, 30, 40]
Pre-allocated array
var arr = [Int]()
arr.reserveCapacity(1000) // avoid reallocs
for i in 0..<1000 {
arr.append(i)
}
Counter pattern
let counts: [String: Int] = words.reduce(into: [:]) { acc, word in
acc[word, default: 0] += 1
}
A note on the conventional discipline
The contemporary Swift data-structures advice:
- Use arrays (
[T]) for ordered sequences. - Use dictionaries (
[K: V]) for key-value lookups. - Use sets (
Set<T>) for unique-value collections. - Use tuples for small unnamed aggregates.
- Use structs for named aggregates (the conventional default).
- Use enums with associated values for sum types and discriminated unions.
- Use
letovervarfor properties. - Use
Codablefor serialisation; rely on synthesised conformance. - Use
[default:]for “increment-or-initialise” patterns. - Use
Setor sorted Array for membership tests. - Use
mutatingmethods on structs that modify properties. - Use protocol conformances (
Hashable,Equatable) for substantial flexibility.
The combination — value-typed collections with COW, generic types with substantial protocol conformances, enums with associated values for sum types, the Codable automatic serialisation, the substantial Sequence/Collection method surface — is the substance of Swift’s data-structure surface. The discipline produces concise, type-safe, expressive code with substantial built-in functionality.