Mastering Generics in Swift: From Basics to Advanced Techniques

Hey there, Swifties! Whether you’re just starting out or looking to level up your Swift game, this post will walk you through everything you need to know about generics, complete with plenty of examples.

At its core, generics allow you to write flexible, reusable functions and types that can work with any type, subject to the requirements you define. They’re a way to tell the Swift compiler that a function or type can work with any type.

Let’s start with a simple example:

Swift
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

This swapTwoValues(_:_:) function can swap two values of any type. The placeholder type T is an example of a type parameter. When you call the function, Swift replaces T with an actual type based on the values you pass in.

Let’s look at how we might use our swapTwoValues(_:_:) function:

Swift
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
print("someString is now \(someString), and anotherString is now \(anotherString)")
// Prints "someString is now world, and anotherString is now hello"

As you can see, we can use the same function to swap integers and strings. Pretty cool, right?

Now, let’s step it up a notch and look at generic types. Here’s a generic stack implementation:

Swift
struct Stack<Element> {
    var items: [Element] = []
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

We can create stacks of any type:

Swift
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

let fromTheTop = stackOfStrings.pop()
print(fromTheTop)  // Prints "tres"

Sometimes, we want to use generics but need to make sure the types we’re working with have certain capabilities. That’s where type constraints come in. Let’s look at an example:

Swift
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

Here, we’re constraining T to types that conform to the Equatable protocol. This ensures we can use the == operator to compare values.

Associated types are a powerful feature that allow protocols to be generic. Let’s look at the Container protocol:

Swift
protocol Container {
    associatedtype Item
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

We can implement this protocol with our Stack type:

Swift
extension Stack: Container {
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

Generic where clauses allow you to define requirements for associated types. Here’s an example:

Swift
func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.Item == C2.Item, C1.Item: Equatable {
        
    // Check that both containers contain the same number of items
    if someContainer.count != anotherContainer.count {
        return false
    }
    
    // Check each pair of items to see if they're equivalent
    for i in 0..<someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    
    // All items match, so return true
    return true
}


//Usage

var stackOfStrings1 = Stack<String>()
stackOfStrings.push("uno")
                
var stackOfStrings2 = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")

print(allItemsMatch(stackOfStrings1, stackOfStrings2))
// Prints false

This function checks if two containers have the same items in the same order. The where clause specifies that the Item types of both containers must be the same and must conform to Equatable.

Introduced in Swift 5.1, opaque types allow you to hide the concrete return type of a function or method. They’re defined using the some keyword:

Swift
protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    func draw() -> String {
        return "▲"
    }
}

struct Square: Shape {
    func draw() -> String {
        return "■"
    }
}

func makeShape() -> Shape {
    let someCondition = Bool.random()
    return someCondition ? Triangle() : Square()
}

The makeShape() function returns “Shape” – we know it returns a Shape, but we don’t know (and don’t need to know) which specific type of Shape it is.

Remember, the key to really understand these concepts is to run each example and modify them one by one according to your own scenario or use case.


Posted

in

Tags: