Swift generics enable you to create flexible, reusable functions and types that work with any type. The Swift standard library itself is built on generics—collections like Array and Dictionary are generic types. This means you can create an array of integers, strings, or any other Swift type without duplicating code.
Let's start with a non-generic function that swaps two integer values:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 100
var y = 200
print("Before swap: \(x) and \(y)")
swapTwoInts(&x, &y)
print("After swap: \(x) and \(y)")
Output:
Before swap: 100 and 200
After swap: 200 and 100
This works only for Int. To swap String or Double values, you'd need separate functions like swapTwoStrings and swapTwoDoubles, leading to code duplication. Generics solve this by introducing a placeholder type (commonly T).
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
Now you can swap any type:
var num1 = 100
var num2 = 200
print("Before: \(num1) and \(num2)")
swapTwoValues(&num1, &num2)
print("After: \(num1) and \(num2)")
var str1 = "Hello"
var str2 = "World"
print("Before: \(str1) and \(str2)")
swapTwoValues(&str1, &str2)
print("After: \(str1) and \(str2)")
Output:
Before: 100 and 200
After: 200 and 100
Before: Hello and World
After: World and Hello
Generic Types
Swift lets you define your own generic types—classes, structs, or enums—that can work with any type, just like Array and Dictionary. Consider a generic Stack collection: elements are added (pushed) and removed (popped) only from the end.
Here is a non-generic version for integers:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
And the generic version using a placeholder Element:
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
var stringStack = Stack<String>()
stringStack.push("apple")
stringStack.push("banana")
print(stringStack.items) // ["apple", "banana"]
let popped = stringStack.pop()
print("Popped: \(popped)") // banana
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
print(intStack.items) // [10, 20]
Output:
["apple", "banana"]
Popped: banana
[10, 20]
Element acts as a placeholder for the actual type used when the stack is instantiated.
Extending a Generic Type
When you extend a generic type, you don't need to repeat the type parameter list. The original type parameters are available within the extension. Here's an extension that adds a read-only computed property to peek at the top item without removing it:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items.last
}
}
if let top = stringStack.topItem {
print("Top item is \(top)")
}
Output:
["apple"]
Top item is apple
Type Constraints
Type constraints specify that a type parameter must inherit from a certain class or conform to a specific protocol or protocol composition. Syntax:
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// body
}
Example: Find the index of a value in an array, but only if the type is Equatable:
func findIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (index, element) in array.enumerated() {
if element == value {
return index
}
}
return nil
}
let names = ["Alice", "Bob", "Charlie"]
if let idx = findIndex(of: "Bob", in: names) {
print("Bob found at index \(idx)")
}
Output:
Bob found at index 1
Associated Types
Protocols can define associated types using the associatedtype keyword. The protocol Container below requires an ItemType and three capabilities: append, count, and subscript access.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Stack<Element>: Container {
var items = [Element]()
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element { return items.removeLast() }
// Conform to Container
mutating func append(_ item: Element) { self.push(item) }
var count: Int { return items.count }
subscript(i: Int) -> Element { return items[i] }
}
var stack = Stack<String>()
stack.push("one")
stack.push("two")
stack.push("three")
print(stack.items) // ["one", "two", "three"]
print(stack.count) // 3
Output:
["one", "two", "three"]
3
Array already has append, count, and subscript, so it can conform to Container with an empty extension:
extension Array: Container {}
Where Clauses
Where clauses let you specify additional constraints on type parameters and their associated types. The function below checks if two containers contain the same elements in the same order:
func allItemsMatch<C1: Container, C2: Container>(
_ first: C1,
_ second: C2
) -> Bool where C1.Item == C2.Item, C1.Item: Equatable {
guard first.count == second.count else { return false }
for i in 0..<first.count {
if first[i] != second[i] { return false }
}
return true
}
let stackA = Stack<String>()
stackA.push("cat")
stackA.push("dog")
let arrayB: [String] = ["cat", "dog"]
if allItemsMatch(stackA, arrayB) {
print("All items match!")
} else {
print("Items do not match")
}
Output:
All items match!
Generics in Swift provide a powerful way to write polymorphic code without sacrificing type safety. By mastering placeholder types, constraints, associated types, and where clauses, you can build robust, reusable abstractions.