Generics in Swift Explained

Written by Reinder de Vries on June 15 2020 in App Development

Generics in Swift Explained

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

Here’s what we’ll get into:

  • What problems do generics solve?
  • Placeholder types and generic functions
  • Generic type constraints with protocols
  • Working with associated types
  • Combining protocols and generics

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

Ready? Let’s go.

  1. How to Use Generics in Swift
  2. Working with Generic Functions and Placeholder Types
  3. Working with Generic Type Constraints
  4. Combining Generics, Protocols and Associated Types
  5. Further Reading

How to Use Generics in Swift

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. Don’t repeat yourself.

Can make your code reusable, without needing to specifically define the types the addition(a:b:) function can work with? Yes! That’s where generics come in. You can use ’em to create functions that work with many types.

Let’s find out how.

Working with 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
}

What’s going on in the function? Let’s break it 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. You can see this in the above Swift code – make sure to check that!

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

Like this:

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 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

Working with Generic Type Constraints

Here’s that generic function once more:

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

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 cannot use our addition(a:b:) function to add two UIViewController or UILabel objects. That wouldn’t make sense! You can only use it for values that conform to the Numeric protocol.

Let’s look at another example. This is 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 above function searches for parameter foundItem in the items array, by comparing it against 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. This makes sense — you want to look for a T value in an array of T values.

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. Otherwise we won’t be able to use the == operator.

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 == operator (as a function). The operator == 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. Let’s move on, and discuss protocols and generics in more detail.

Combining 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 (positively) 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 required 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(). 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 struct that defines a title and author.

struct Book {
    var title = ""
    var 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]
    }
}

The above Bookcase class stores books in a books array. It adopts the functions from the Storage protocol, to store and retrieve books. You can only use Bookcase to store Book objects.

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

We’re going to make a few changes to the code. First, we’ll need to add an associated type to Storage. 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. Because we’re working with protocols here, the adopting 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. It’s an implementation detail.

We’re now going to implement the Storage protocol in a Trunk class. This trunk will be able to store any kind of item, not just books.

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 the class definition for Trunk includes <Item>? This is a placeholder, and it’s used throughout the class. 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 struct 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 struct, instead of the Item placeholder.

The code 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…

How does this work? It’s a play between the associated type and the generic placeholder.

  • The protocol Storage defines an associated type. This type must be determined by the class that adopts the Storage protocol.
  • The Trunk class uses a generic placeholder to implement its functions.

Ultimately, the associated type and generic placeholder are concretized when we define Trunk with Book. This indicates the actual types that the code is going to use. When writing our implementation, we have flexibility to define these types ourselves.

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!

Since Swift 5.1, we can use another approach to working with generics: opaque types. The some keyword lets you “hide” the concrete return type of a property or function. The concrete type that’s returned can be determined by the implementation itself, as opposed to the calling code. This is why opaque types are sometimes called reverse generics.

Learn how to build iOS apps

Get started with iOS 13 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 13 apps with Swift 5 and Xcode 11.

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.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy