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.

Leave a Comment

Your email address will not be published. Required fields are marked *