Generics in Swift Explained

Written by Reinder de Vries on August 11 2018 in App Development

Generics in Swift Explained

Generics are one of the most powerful features of the Swift programming language. At first, they can be confusing! In this article we’re going to take a look at how generics work, and what you can do with them.

Before you continue, you should have a basic understanding of Swift’s type system, classes and protocols.

Ready? Let’s go.

  1. What Are Generics?
  2. Using Generic Functions And Placeholder Types
  3. Generic Type Constraints
  4. Generics, Protocols And Associated Types
  5. Further Reading

What Are Generics?

You already know that Swift has a strong type system. Once a variable is declared as a string, you can’t just assign an integer value to it.

Like this:

var text:String = "Hello world!"
text = 5
// Output: error: cannot assign value of type 'Int' to type 'String'

Once a string, always a string! You can’t assign a value of type Int to a variable of type String.

Strictness is generally a good thing, because it helps you avoid programming errors. But what if you want to work with data types that aren’t so strict?

Let’s look at an example. Let’s say you’ve created a simple function that adds one number to another. Like this:

func addition(a: Int, b: Int) -> Int
{
    return a + b
}

let result = addition(a: 42, b: 99)
print(result)
// Output: 141

The function takes two parameters a and b of type Int, and returns a value of type Int. The + operator adds the numbers, and returns the result.

What if you want to expand your function to also add other number types, such as Float and Double? It wouldn’t be much of a function if it can’t add decimal-point numbers!

So, you write a new function…

func addition(a: Double, b: Double) -> Double
{
    return a + b
}

Hmm. You’ve now repeated your code! That’s generally a bad thing, as per the DRY principle.

Can make your code reusable, without specifically defining the types the addition(a:b:) function can work with? Yes! That’s where generics come in.

Using Generic Functions And Placeholder Types

With generics you can write clear, flexible and reusable code. You avoid writing the same code twice, and it lets you write generic code in an expressive manner.

Let’s take the original addition(a:b:) function, and turn it into a generic function. Like this:

func addition<T: Numeric>(a: T, b: T) -> T
{
    return a + b
}

Let’s break that down:

  • Instead of Int, the parameters and return type of the function are of type T. This is called a placeholder type.
  • The placeholder type T is defined with <T: Numeric>. This tells Swift that T isn’t an actual type, but a placeholder within the addition(a:b:) function.

The placeholder T doesn’t specify what the type of T exactly is, just that both parameters a and b, and the function return value, need to be the same type T. The inputs and output of the function need to have the same type.

The generic addition(a:b:) function can now work with any type, as long as it conforms to Numeric (more on that later). The function is able to add integers, doubles, floating point values, etcetera. It’s reusable and flexible, without needlessly repeating code.

let a = addition(a: 42, b: 99)
print(a)
// Output: 141

let b = addition(a: 0.99, b: 0.33)
print(b)
// Output: 1.32

let c = addition(a: Double.pi, b: Double.pi)
print(c)
// Output: 6.28318530717959

Awesome! Let’s move on to type constraints!

Learn how to build iOS apps

Get started with iOS 12 and Swift 4

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.

Generic Type Constraints

The <T: Numeric> syntax adds a type constraint to the placeholder. It defines that T needs to conform to the protocol Numeric. This is a built-in Swift protocol for any numeric values, like Int and Double.

Said differently, you can’t use our addition(a:b:) function to add two UIViewController or UILabel objects. That wouldn’t make sense!

Let’s look at another example. Here’s a generic function that can find the index of a value in an array.

func findIndex<T>(of foundItem: T, in items: [T]) -> Int?
{
    for (index, item) in items.enumerated()
    {
        if item == foundItem {
            return index
        }
    }

    return nil
}

The function searches for foundItem in the items array, by comparing every item in a loop. When a match is found, it returns the index of the found item. The function returns nil when it can’t find the item, so the return type of findIndex(of:in:) is Int?.

The placeholder type T is used in the function declaration. It tells Swift that this function can find any item in any array, as long as the foundItem and items in the array are of the same type.

Here’s how you use the function:

let names = ["Ford", "Arthur", "Trillian", "Zaphod", "Deep Thought"]

if let result = findIndex(of: "Zaphod", in: names) {
    print(result)
}

// Output: 3

Unfortunately, the above function doesn’t compile! We need to set another type constraint on T. We’re using the equality operator == in the function to determine if two items are equal, and that means that T needs to conform to the Equatable protocol.

Like this:

findIndex<T: Equatable>(of foundItem: T, in items: [T]) -> Int?

The Equatable protocol, as its name implies, is a protocol that declares the == function. This function is used to determine if two values are equal.

Swift provides a few of these basic protocols:

  • Equatable for values that can be equal, or not equal
  • Comparable for values that can be compared, like a > b
  • Hashable for values that can be “hashed”, which is a unique integer representation of that value (often used for dictionary keys)
  • CustomStringConvertible for values that can be represented as a string, a helpful protocol for quickly turning custom objects into printable strings
  • Numeric and SignedNumeric for values that are numbers, like 42 and 3.1415
  • Strideable for values that can offset and measured, like sequences, steps and ranges

And of course, you can define your own protocols that generic placeholder types need to conform to.

Generics, Protocols And Associated Types

So far we’ve looked at generic functions that use placeholder values to loosely specify the input and output of the function, and protocols to constrain those types. Let’s continue with an approach to generics that will make your head spin…

You’ve heard of protocols before, right? Here’s a quick refresher: A protocol specifies functions that a conforming class needs to adopt. When it adopts the functions, the class is said to conform to the protocol.

Let’s take a look at an example. Imagine you’ve got a restaurant that sells certain food items. A customer comes into your restaurant, and wants to eat something. He doesn’t really care what he’s going to eat exactly, as long as it’s edible…

The customer has defined this protocol:

protocol Edible {
    func eat()
}

Any class that wants to conform to Edible needs to implement the eat() function, like this:

class Apple: Edible
{
    func eat()
    {
        print("Omnomnom!")
    }
}

Protocols help you write flexible and reusable code. They also help you to loosly-couple your code. The customer doesn’t need to know the exact implementation of what he’s going to eat, only that it has a function eat(). And he can eat anything, as long as it’s Edible!

But what does this have to do with generics?

Let’s start with another hypothetical scenario. You’re going to a department store, like IKEA, to buy a bookcase. And you have two requirements for that bookcase:

  • You don’t necessarily want to put books in the bookcase
  • It doesn’t even need to be a bookcase, it can also be a storage box, a locker, a closet, or a wardrobe
  • Heck… you just want “something” that you can put “items” in, and take items out

You feel where this is going. You’re going to define a generic protocol and take it to IKEA, aren’t ya!?

OK, let’s start with the Storage protocol:

protocol Storage
{
    func store(item: Book)
    func retrieve(index: Int) -> Book
}

The protocol Storage declares two functions, one to store a book and one to retrieve a book, by its index. For the sake of completeness, assume that Book is a simple class that defines a title and author.

Any class can adopt the Storage protocol to store and retrieve books, such as Bookcase and Booktrunk classes. Like this:

class Bookcase: Storage
{
    var books = [Book]()

    func store(item: Book) 
    {
        books.append(item)
    }

    func retrieve(index: Int)
    {
        return books[index]
    }
}

ou didn’t just want to store books, however! You wanted to store any item in any storage. This is where generics come in.

You can define a generic type in a protocol by using an associated type. It’s kinda like a placeholder type, as we’ve seen before, but then for protocols.

Like this:

protocol Storage
{
    associatedtype Item
    func store(item: Item)
    func retrieve(index: Int) -> Item
}

Here’s what has changed:

  • You’ve added the Item associated type with the associatedtype keyword
  • The store(item:) and retrieve(index:) functions now use that associated type Item

See how that’s similar to the placeholder type? Instead of just books, the class that conforms to the Storage protocol can now store any type of item. Moreover, such a class can determine how it stores these items too.

Think of the associated type as associating a generic type with a protocol without defining the type itself. The generic type itself is not yet defined, because that’s up to the class that adopts the protocol.

Let’s implement the Storage protocol in a Trunk class. This trunk will be able to store any kind of item.

class Trunk<Item>: Storage
{
    var items:[Item] = [Item]()

    func store(item: Item)
    {
        items.append(item)
    }

    func retrieve(index: Int) -> Item
    {
        return items[index]
    }
}

See how we’re using Item in the class declaration? Again, we’re not specifying an actual type – just a placeholder. The Trunk class has a simple array that can store and retrieve items.

We are now going to create a trunk to store books. We’ve defined our Book class earlier, so now it just comes down to filling the trunk with books:

let bookTrunk = Trunk<Book>()
bookTrunk.store(item: Book(title: "1984", author: "George Orwell"))
bookTrunk.store(item: Book(title: "Brave New World", author: "Aldous Huxley"))
print(bookTrunk.retrieve(index: 1).title)
// Output: Brave New World

On the first line of code, an actual type Book is used when declaring the type of bookTrunk. At this point the Trunk class uses the Book class, instead of Item.

The class itself remains flexible, of course. What if we define a Shoe class, with a size and a brand. Can we also store that in a trunk? Yes!

let shoeTrunk = Trunk<Shoe>()
shoeTrunk.store(item: Shoe(size: 42, brand: "Nike"))
shoeTrunk.store(item: Shoe(size: 99, brand: "Adidas"))
print(shoeTrunk.retrieve(index: 0).brand)
// Output: Nike

OK, and now for the pièce de résistance… Can we also make a shoebox, a bookcase, or even a freight ship with the Storage protocol? Yes!

An example:

class FreightShip<Item>: Storage
{
    func store(item: Item)
    {
        // Load onto the ship...
    }

    func retrieve(index: Int) -> Item
    {
        // Unload from the ship...
    }
}

You can store shipping containers on the FreightShip, or Shoe objects, or even Book items. It’s up to you…

The generic Storage protocol only specifies that whatever class adopts it needs to include a function to store any item, and retrieve any item.

It doesn’t specify how this item needs to be stored or retrieved, or what kind of item it can be. As a result, we can create any kind of storage that can store any kind of item.

Magical!

Learn how to build iOS apps

Get started with iOS 12 and Swift 4

Sign up for our iOS development course Zero to App Store and learn how to build professional iOS 12 apps with Swift 4 and Xcode 10.

Further Reading

Generics are super powerful, but can be challenging to comprehend. When do you use generics as a practical app developer?

It depends. When you’re defining your app architecture, it helps to think about how loosely or tightly you’re coupling your code. Can you reuse some of your code by using generics?

Swift uses a lot of generics behind the scenes, and so do popular frameworks like Alamofire. Even if you don’t end up using your own generics in your app, it helps to understand how other frameworks and SDKs rely on generics to make their code more reusable.

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.