top of page

Generics in Swift: A Comprehensive Guide

  • Writer: arda doğantemur
    arda doğantemur
  • Feb 18
  • 9 min read


Generics are one of the most powerful features of Swift, allowing you to write flexible and reusable code. Whether you're a beginner or an experienced Swift developer, mastering generics can significantly improve the way you design and structure your code.

In this blog post, we'll go deep into generics, covering their purpose, how they work, and some advanced techniques. By the end, you'll have a solid understanding of how to use generics effectively in Swift.



  1. What Are Generics?

Generics allow us to write code that works with any data type while maintaining type safety. Instead of writing duplicate code for different types, we can define a generic function, structure, or class that works with multiple types.


Why Do We Need Generics?

Consider a function that swaps two integers:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

Now, if we want to swap two doubles, we'd need another function:

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temp = a
    a = b
    b = temp
}

This quickly becomes repetitive. Instead, we can define a generic function that works with any type.



  1. What Is a Generic Function?

generic function allows us to write functions that work with any type, using type parameters enclosed in angle brackets (<T>).

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

Now, we can use swapValues with integers, doubles, or any other type.

var x = 10, y = 20
swapValues(&x, &y) // Works for Int

var a = 3.5, b = 5.2
swapValues(&a, &b) // Works for Double

  1. Generic Types

Why Use Generic Types? Generic types allow us to create flexible and reusable data structures without specifying a concrete type in advance.


Problem: Implementing a Type-Specific Stack

Without generics, we’d need separate implementations for different types:

struct IntStack {
    private var elements: [Int] = []
    mutating func push(_ element: Int) {
        elements.append(element)
    }
    mutating func pop() -> Int? {
        return elements.popLast()
    }
}
struct StringStack {
    private var elements: [String] = []
    
    mutating func push(_ element: String) {
        elements.append(element)
    }
    
    mutating func pop() -> String? {
        return elements.popLast()
    }
}

To support more types, we’d have to duplicate this struct.

Solution: Using a Generic Type
struct Stack<T> {
    private var elements: [T] = []
    mutating func push(_ element: T) {
        elements.append(element)
    }
    mutating func pop() -> T? {
        return elements.popLast()
    }
}

Now, we can create stacks for any type:

var intStack = Stack<Int>()
intStack.push(5)
print(intStack.pop()) // 5

var stringStack = Stack<String>()
stringStack.push("Swift")
print(stringStack.pop()) // "Swift"
🏋️‍♂️ Practice Question

Write a generic structure called Queue<T> that follows the FIFO (First In, First Out) principle.

• It should have enqueue(_:) to add an item.

• It should have dequeue() to remove and return the first item.


Solution Implementing a Generic Queue

A queue follows the FIFO (First In, First Out) principle. We can implement a generic queue in Swift using an array:

struct Queue<T> {
    private var elements: [T] = []
    
    mutating func enqueue(_ element: T) {
        elements.append(element)
    }
    
    mutating func dequeue() -> T? {
        return elements.isEmpty ? nil : elements.removeFirst()
    }
}
var numberQueue = Queue<Int>()
numberQueue.enqueue(1)
numberQueue.enqueue(2)
numberQueue.enqueue(3)

print(numberQueue.dequeue()) // Output: Optional(1)
print(numberQueue.dequeue()) // Output: Optional(2)
print(numberQueue.dequeue()) // Output: Optional(3)
print(numberQueue.dequeue()) // Output: nil (Queue is empty)
How It Works:

• enqueue(_:) adds a new element to the end of the queue.

• dequeue() removes and returns the first element in the queue.

• If the queue is empty, dequeue() returns nil.

This Queue<T> can store any type while keeping type safety intact! 🎯



  1. Generic Constraints

Why Use Constraints? Sometimes, we want our generic type to work only with specific types, such as those that conform to a protocol.

Problem: Defining a Generic Function Without Constraints
func compareValues<T>(a: T, b: T) -> Bool {
    return a == b // ❌ Error: `T` may not support `==`
}
Solution: Adding a Constraint
func compareValues<T: Equatable>(a: T, b: T) -> Bool {
    return a == b // ✅ Now it works!
}

Now compareValues only works for types that conform to Equatable.


🏋️‍♂️ Practice Question: Using Constraints

Write a generic function called findIndex<T>(of element: T, in array: [T]) -> Int?

• It should return the index of the first occurrence of element in array.

• Use a constraint to ensure T conforms to Equatable, so the function can compare elements using ==.


✅ Solution: Implementing findIndex with Constraints

We need to restrict T to types that conform to Equatable, so we can safely use == for comparisons.

func findIndex<T: Equatable>(of element: T, in array: [T]) -> Int? {
    for (index, item) in array.enumerated() {
        if item == element {
            return index
        }
    }
    return nil
}
let numbers = [10, 20, 30, 40]
print(findIndex(of: 30, in: numbers)) // Output: Optional(2)
print(findIndex(of: 50, in: numbers)) // Output: nil

let words = ["Swift", "Generics", "Constraints"]
print(findIndex(of: "Generics", in: words)) // Output: Optional(1)
  • We used T: Equatable to restrict T to only types that support ==.

  • The function returns nil if the element is not found.

  • It works for any Equatable type (e.g., Int, String, custom types that conform to Equatable).



  1. Associated Types in Protocols

Why use associated types? When defining a protocol, we may not know the exact type it will work with.

Problem: Using a Fixed Type in a Protocol

protocol Container {
    func add(_ item: Int) // ❌ Limited to Int only
}
Solution: Using associatedtype
protocol Container {
    associatedtype Item
    func add(_ item: Item)
}

This makes the protocol more flexible.


🏋️‍♂️ Practice Question: Using Associated Types in Protocols

Write a protocol called Storage that:

• Has an associated type called Element.

• Defines a method store(_ item: Element).

• Defines a method retrieve() -> Element? to get the last stored item.

Then, create a Box<T> struct that conforms to Storage and stores a single item.


✅ Solution: Implementing Storage with Associated Types

Define the Storage Protocol

protocol Storage {
    associatedtype Element
    mutating func store(_ item: Element)
    mutating func retrieve() -> Element?
}

Implement Box<T> Struct

struct Box<T>: Storage {
    private var item: T?
    
    mutating func store(_ item: T) {
        self.item = item
    }
    
    mutating func retrieve() -> T? {
        return item
    }
}
var intBox = Box<Int>()
intBox.store(42)
print(intBox.retrieve()) // Output: Optional(42)

var stringBox = Box<String>()
stringBox.store("Swift")
print(stringBox.retrieve()) // Output: Optional("Swift")
  • associatedtype allows protocols to work with any type.

  • The Storage protocol is generic, enabling multiple implementations.

  • Box<T> conforms to Storage while keeping type safety.



  1. Generic Extensions

Why User generic extension? Extensions in Swift allow us to add functionality to existing types, including generic types. When working with generics, extensions help us keep our code clean and reusable by organizing additional methods separately from the main type definition.

This is useful when:

• We don’t want to modify the original code directly.

• The type exists in another module (e.g., a framework or library) where we don’t have permission to edit it.

• We want to keep the original definition clean and add functionality separately.


Problem: Adding Extra Functionality to a Generic Type

Let’s revisit our generic Stack<T> and extend it to add useful properties and methods.

Imagine we have a generic Stack<T>, but it’s defined in a different module or a shared framework that we cannot modify:

struct Stack<T> {
    private var elements: [T] = []
    
    mutating func push(_ element: T) {
        elements.append(element)
    }
    
    mutating func pop() -> T? {
        return elements.popLast()
    }
}

So far, our Stack<T> can push and pop elements, but what if we also need a method to check if the stack is empty?

Since we can’t modify the original code, we can use an extension to add this functionality without changing the existing definition.


Solution: Extending a Generic Type
extension Stack {
    var isEmpty: Bool {
        return elements.isEmpty
    }
}

Now, any instance of Stack<T> can use isEmpty, even though we didn’t touch the original implementation:

var numberStack = Stack<Int>()
print(numberStack.isEmpty) // Output: true

numberStack.push(10)
print(numberStack.isEmpty) // Output: false

This approach keeps our original Stack<T> unchanged, making it useful for extending third-party libraries or frameworks where modification isn’t possible.


🏋️‍♂️ Practice Question: Generic Extensions

We previously implemented a Queue<T> struct that works as a FIFO (First In, First Out) queue.


👉 Problem: Our queue is missing some useful operations. Let’s fix that!

🔹 Your Task: Extend Queue<T> to:

1️⃣ Add a computed property count that returns the number of elements.

2️⃣ Add a method peek() that returns the first element without removing it.

📝 Hint: Since Queue<T> might exist in another module, assume you cannot modify its original definition—you can only use an extension.


✅ Solution: Extending Queue<T> with Useful Methods

Since we cannot modify the original Queue<T> structure, we’ll extend it to add the required functionality.


Extend Queue<T> to Add count and peek()

extension Queue {
    var count: Int {
        return elements.count
    }
    
    func peek() -> T? {
        return elements.first
    }
}

🛠 How It Works:

count: A computed property that returns the total number of elements in the queue.

peek(): Returns the first element without removing it.

var numberQueue = Queue<Int>()
numberQueue.enqueue(10)
numberQueue.enqueue(20)
numberQueue.enqueue(30)

print(numberQueue.count) // Output: 3
print(numberQueue.peek() ?? "Empty") // Output: 10

numberQueue.dequeue()
print(numberQueue.count) // Output: 2
print(numberQueue.peek() ?? "Empty") // Output: 20
  • Extensions allow us to enhance generic types without modifying their original definition.

  • We can add computed properties and methods to generic types using extensions.

  • This approach is useful for extending third-party libraries or frameworks where modification isn’t possible.



  1. Type Constraints in Extensions

So far, we’ve learned how to extend generic types to add new functionality. But what if we only want to extend a generic type when its generic parameter meets a specific condition?

This is where type constraints in extensions come in!


Problem: Extending a Generic Type for Specific Data Types Only

Imagine we have our generic Stack<T> again:

struct Stack<T> {
    private var elements: [T] = []
    
    mutating func push(_ element: T) {
        elements.append(element)
    }
    
    mutating func pop() -> T? {
        return elements.popLast()
    }
}

Now, we want to add a method to calculate the sum of elements, but that only makes sense if T is a numeric type (like Int or Double).

If we try to add this method to all types, we might run into errors because not all types support addition (+).


Solution: Using Type Constraints in Extensions

We can limit the extension to only apply when T conforms to Numeric:

extension Stack where T: Numeric {
    func sum() -> T {
        return elements.reduce(0, +)
    }
}
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print(intStack.sum()) // Output: 60

var doubleStack = Stack<Double>()
doubleStack.push(2.5)
doubleStack.push(3.5)
print(doubleStack.sum()) // Output: 6.0

var stringStack = Stack<String>()
// stringStack.sum() ❌ Error: `sum()` is only available for Numeric types
  • Now, the sum() method is only available when T conforms to Numeric!


🏋️‍♂️ Practice Question: Type Constraints in Extensions

We previously implemented a Queue<T> struct.

👉 Problem: Extend Queue<T> to:

  • Add a method average() only for numeric types (Int, Double, etc.).

  • average() should return the average of all elements in the queue.

📝 Hint: Use where T: Numeric to restrict the extension to numeric types.


✅ Solution: Extending Queue<T> with Type Constraints

Since we only want to add the average() method for numeric types, we’ll use a type constraint (where T: Numeric) in our extension.


Extend Queue<T> for Numeric Types Only

extension Queue where T: Numeric {
    func average() -> Double {
        guard !elements.isEmpty else { return 0 }
        
        let total = elements.reduce(0, +)
        return Double("\(total)")! / Double(elements.count)
    }
}

🛠 How It Works:

We use where T: Numeric to ensure this extension only applies to numeric types (Int, Double, etc.).

• reduce(0, +) calculates the total sum of elements.

Convert total and count to Double for precise division.

var numberQueue = Queue<Int>()
numberQueue.enqueue(10)
numberQueue.enqueue(20)
numberQueue.enqueue(30)

print(numberQueue.average()) // Output: 20.0

var doubleQueue = Queue<Double>()
doubleQueue.enqueue(5.5)
doubleQueue.enqueue(6.5)

print(doubleQueue.average()) // Output: 6.0

var stringQueue = Queue<String>()
// stringQueue.average() ❌ Error: `average()` is only available for Numeric types
  • where T: Numeric ensures average() is only available for numeric types.

  • Keeps the original Queue<T> generic while allowing type-specific behavior.

  • Avoids runtime errors by restricting average() to valid types.


Continue reading: Check out Part 2: Generics reloaded.. to dive deeper into the topic!

Comments


© 2020 by Arda Doğantemur.

bottom of page