Introduction: Why You Can't Do Without Generics in Swift?
Imagine you need to write a function that adds two numbers. Then a function for adding two strings. And then for arrays. Without generic programming (Generics), you would have to create a separate version of the code for each data type. This not only increases the amount of code but also complicates its maintenance.
Generics in Swift are a powerful tool that allows you to write flexible, reusable, and type-safe code. They are the foundation of popular structures like Array, Dictionary, and Optional. In this article, we will break down why Generics are needed, how to use them in practice, and why every beginner iOS/macOS developer should master this topic.
What Are Generics in Simple Terms?
Generics are a way to write code that works with any data type without sacrificing Swift's strong typing. Instead of specifying a concrete type (e.g., Int or String), you use a placeholder (most often the letter T), which is replaced with the actual type when the function is called or the structure is created.
The Problem Without Generics
Suppose you write a function to swap the values of two variables. Without Generics, you would have to write it like this:
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}This is obvious duplication. If you need to swap Double or custom types, the number of functions will grow.
The Solution with Generics
One universal function replaces all the previous ones:
func swapValues<T>(_ a: inout T, _ b: inout T) { let temp = a a = b b = temp}
// Usage:var x = 10var y = 20swapValues(&x, &y) // x = 20, y = 10
var firstName = "Anna"var lastName = "Ivanova"swapValues(&firstName, &lastName) // firstName = "Ivanova"The angle brackets <T> declare the type placeholder. The compiler automatically determines that T is Int in the first case and String in the second. The code becomes concise and safe.
How to Use Generics: Main Scenarios
Generics in Swift are used in functions, structures, classes, enumerations, and protocols. Let's look at the most common cases.
1. Generic Functions
We already saw the example with swapValues. But you can go further — create a function that finds the index of an element in an array:
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 numbers = [1, 5, 9, 12]if let index = findIndex(of: 9, in: numbers) { print("Index: \\(index)") // Index: 2}Notice the <T: Equatable>. This is a type constraint. We are saying: "T must conform to the Equatable protocol so we can use the == operator." Without this constraint, the code would not compile.
2. Generic Types (Structures and Classes)
Let's create a simple "Stack" structure (LIFO) that can store any type:
struct Stack<Element> { private var items: [Element] = []
mutating func push(_ item: Element) { items.append(item) }
mutating func pop() -> Element? { return items.popLast() }
func peek() -> Element? { return items.last }}
// Usage:var intStack = Stack<Int>()intStack.push(1)intStack.push(2)print(intStack.pop