Swift Generics
Generics let you write flexible, reusable code that works with any type. Instead of writing a separate function for integers and another for strings, you write one generic function that handles both. Swift's standard library — arrays, dictionaries, optionals — is built almost entirely with generics.
The Storage Box Analogy
┌──────────────────────────────────────────────────┐
│ Without Generics: │
│ Box for Books, Box for Shoes, Box for Toys... │
│ You need a different box design for each item. │
│ │
│ With Generics: │
│ One universal box design → fits anything │
│ Box<Book>, Box<Shoe>, Box<Toy> │
│ │
│ One design, many uses. │
└──────────────────────────────────────────────────┘
The Problem Generics Solve
// Without generics — repeated code
func swapInts(_ a: inout Int, _ b: inout Int) {
let temp = a; a = b; b = temp
}
func swapStrings(_ a: inout String, _ b: inout String) {
let temp = a; a = b; b = temp
}
// With generics — one function handles all types
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a; a = b; b = temp
}
var x = 10, y = 20
swapValues(&x, &y)
print(x, y) // 20 10
var name1 = "Alice", name2 = "Bob"
swapValues(&name1, &name2)
print(name1, name2) // Bob Alice
The <T> is a type parameter — a placeholder for any actual type. Swift fills in T based on what you pass in at the call site.
Generic Struct
struct Stack<T> {
private var items: [T] = []
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T? {
return items.popLast()
}
var top: T? {
return items.last
}
var isEmpty: Bool {
return items.isEmpty
}
}
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
intStack.push(3)
print(intStack.pop()!) // 3
print(intStack.top!) // 2
Stack Visualised
┌──────────────────────────────────────────────────┐
│ Stack = A pile of plates │
│ │
│ push(1) → [1] │
│ push(2) → [1, 2] │
│ push(3) → [1, 2, 3] │
│ pop() → returns 3 → [1, 2] │
│ │
│ Last in, first out (LIFO) │
└──────────────────────────────────────────────────┘
Type Constraints
func findMax<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
print(findMax(10, 25)) // 25
print(findMax("Apple", "Mango")) // Mango
The constraint T: Comparable means T must support the > operator. Without this, Swift cannot guarantee the comparison works. Constraints keep generics safe.
Generic Function with Multiple Type Parameters
func pair<A, B>(first: A, second: B) -> String {
return "\(first) and \(second)"
}
print(pair(first: 42, second: "Swift")) // 42 and Swift
print(pair(first: true, second: 3.14)) // true and 3.14
Use different letters (A, B) when a function works with two independent types at once.
Associated Types in Protocols
protocol Container {
associatedtype Item
var count: Int { get }
func item(at index: Int) -> Item
}
struct NumberBox: Container {
private var numbers = [10, 20, 30]
var count: Int { numbers.count }
func item(at index: Int) -> Int {
return numbers[index]
}
}
let box = NumberBox()
print(box.item(at: 1)) // 20
Associated types make protocols generic. The protocol says "there will be an Item type" — each adopting type decides what that type actually is.
Where Clauses – Fine-Tuned Constraints
func allEqual<T: Equatable>(_ array: [T]) -> Bool {
guard let first = array.first else { return true }
return array.allSatisfy { $0 == first }
}
print(allEqual([3, 3, 3])) // true
print(allEqual(["a", "a", "b"])) // false
The where clause (or inline constraints like T: Equatable) restricts which types can be used. Only types that support equality checking can be passed to this function.
