An introduction to generics in Swift using its built-in types

donnywals

Donny Wals

Posted on December 5, 2019

An introduction to generics in Swift using its built-in types

Whenever we write code, we want our code to be well-designed. We want it to be flexible, elegant and safe. We want to make sure that Swift’s type system and the compiler catch as many of our mistakes as possible. It’s especially interesting how Swift’s type system can help us avoid obvious errors. For example, Swift won’t allow you to assign an Int to a String property like this:

var anInt = 10
anInt = "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

The Swift compiler would show you an error that explains that you can’t assign a String to an Int and you’d understand this. If something is declared or inferred to be a certain type, it can’t hold any types other than the one it started out as.

It’s not always that simple though. Sometimes you need to write code where you really don’t know what type you’re working with. All you know is that you want to make sure that no matter what happens, that type cannot change. If this doesn’t sound familiar to you, or you’re wondering who in their right mind would ever want that, keep reading. This article is for you.

Reverse engineering Array

A great example of an object that needs the flexibility that I described earlier is an array. Considering that arrays in Swift are created to hold objects of a specific type, whether it’s a concrete type or a protocol, array’s aren’t that different from the mistake I showed you earlier. Let’s adapt the example to arrays so you can see what I mean:

var arrayOfInt = [1, 2, 3]
arrayOfInt = ["one", "two", "three"]
Enter fullscreen mode Exit fullscreen mode

If you try to run this code you will see an error that explains you can’t assign an object of Array<String> to Array<Int>. And this is exactly the kind of magic that you need generics for.

Arrays are created in such a way that they work with any type you throw at them. The only condition being that the array is homogenous, in other words, it can only contain objects of a single type.

So how is this defined in Swift? What does a generic object like Array look like? Instead of showing you the exact implementation from the Swift standard library, I will show you a simplified version of it:

public struct Array<Element> {
  // complicated code that we don’t care about right now
}
Enter fullscreen mode Exit fullscreen mode

The interesting part here is between the angle brackets: <Element>. The type Element does not exist in the Swift standard library. It’s a made-up type that only exists in the context of arrays. It specifies a placeholder that’s used where the real, concrete type would normally be used.

Let’s build a little wrapper around Array that will help you make sense of this a little bit more.

struct WrappedArray<OurElement> {
  private var array = Array<OurElement>()

  mutating func append(_ item: OurElement) {
    array.append(item)
  }

  func get(atIndex index: Int) -> OurElement {
    return array[index]
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice how instead of Element, we use the name OurElement. This is just to prove that Element really doesn’t exist in Swift. In the body of this struct, we create an array. We do this by using its fully written type Array<OurElement>(). The same can be achieved using the following notation: [OurElement](). The outcome is the same.

Next, in the append and get methods we accept and return OurElement respectively. We don’t know what OurElement will be. All we know is that the items in our array, the items we append to it and the items we retrieve from it, will all have the same type.

To use your simple array wrapper you might write something like this:

var myWrappedArray = WrappedArray<String>
myWrappedArray.append("Hello")
myWrappedArray.append("World")
let hello = myWrappedArray.get(atIndex: 0) // "Hello"
let world = myWrappedArray.get(atIndex: 1) // "World"
Enter fullscreen mode Exit fullscreen mode

Neat stuff, right! Try adding an Int, or something else to myWrappedArray. Swift won’t let you, because you specified that OurElement can only ever be String for myWrappedArray by placing String between the angle brackets.

You can create wrapped arrays that hold other types by placing different types between the angle brackets. You can even use protocols instead of concrete types:

var codableArray = WrappedArray<Codable>
Enter fullscreen mode Exit fullscreen mode

The above would allow you to add all kinds of Codable objects to codableArray. Note that if you try to retrieve them from the array using get, you will get a Codable object back, not the conforming type you might expect:

var codableArray = WrappedArray<Codable>

let somethingCodable = Person()
codableArray.append(somethingCodable)
let item = codableArray.get(0) // item is Codable, not Person
Enter fullscreen mode Exit fullscreen mode

The reason for this is that get returns OurElement and you specified OurElement to be Codable.

Similar to arrays, Swift has generic objects for Set (Set<Element>), Dictionary (Dictionary<Key, Value>) and many other objects. Keep in mind that whenever you see something between angle brackets, it’s a generic type, not a real type.

Before we look at an example of generics that you might be able to use in your own code someday, I want to show you that functions can also specify their own generic parameters. A good example of this is the decode method on JSONDecoder:

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
  // decoding logic
}
Enter fullscreen mode Exit fullscreen mode

If you’ve never used this method yourself, it would normally be called as follows:

let result = try? decoder.decode(SomeType.self, from: data) // result is SomeType
Enter fullscreen mode Exit fullscreen mode

Let’s pick apart the method signature for decode a bit:

  • func decode<T>: the decode method specifies that it uses a generic object called T. Again, T is only a placeholder for whatever the real type will be, just like Element and OurElement were in earlier examples.
  • (_ type: T.Type, from data: Data): one of the arguments here is T.Type. This means that we must call this method and specify the type we want to decode data into. In the example, I used SomeType.self. When you call the method with SomeType.self without explicitly specifying T, Swift can infer that T will now be SomeType.
  • throws -> T: This bit marks decode as throwing and it specifies that decode will return T. In the example, T was inferred to be SomeType.
  • where T : Decodable: this last bit of decode's method signature applies a constraint to T. We can make T whatever we want, as long as that object conforms to Decodable. So in our example, we’re only allowed to use SomeType as the type of T if SomeType is decodable.

Take another look at the method signature of decode and let it all sink in for a moment. We’re going to build our own struct in a moment that will put everything together so if it doesn’t make sense yet, I hope it does after the next section.

Applying generics in your code

You have seen how Array and JSONDecoder.decode use generics. Let’s build something relatively simple that applies your newfound logic using an example that I have run into many times over the years.

Imagine you’re building an app that shows items in table views. And because you like to abstract things and separate concerns, you have taken some of your UITableViewDataSource logic and you’ve split that into a view model and the data source logic itself. Yes, I said view model and no, we’re not going to talk about architecture. View models are just a nice way to practice building something with generics for now.

In your app you might have a couple of lists that expose their data in similar ways and heavily simplified, your code might look like this:

struct ProductsViewModel {
  private var items: [Products]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Products {
    return items[indexPath.row]
  }
}

struct FavoritesViewModel {
  private var items: [Favorite]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Favorite {
    return items[indexPath.row]
  }
}
Enter fullscreen mode Exit fullscreen mode

This code is really repetitive, isn’t it? Both view models have similar property and method names, the only real difference is the type of the objects they operate on. Look back to our WrappedArray example. Can you figure out how to use generics to make these view models less repetitive?

If not, that’s okay. Here’s the solution:

struct ListViewModel<Item> {
  private var items: [Item]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Item {
    return item[indexPath.row]
  }
}
Enter fullscreen mode Exit fullscreen mode

Neat, right! And instead of the following code:

let viewModel = FavoritesViewModel()
Enter fullscreen mode Exit fullscreen mode

You can now write:

let viewModel = ListViewModel<Favorite>()
Enter fullscreen mode Exit fullscreen mode

The changes in your code are minimal, but you’ve removed code duplication which is great! Less code duplication means fewer surface areas for those nasty bugs to land on.

One downside of the approach is that you can now use any type of object as Item, not just Favorite and Product. Let’s fix this by introducing a simple protocol and constraining ListViewModel so it only accepts valid list items as Item:

protocol ListItem {}
extension Favorite: ListItem {}
extension Product: ListItem {}

struct ListViewModel<Item> where Item: ListItem {
  // implementation
}
Enter fullscreen mode Exit fullscreen mode

Of course, you can decide to add certain requirements to your ListItem protocol but for our current purposes, an empty protocol and some extensions do the trick. Similar to how decode was constrained to only accept Decodable types for T, we have now constrained ListViewModel to only allow types that conform to ListItem as Item.

Note

Sometimes the where is moved into the angle brackets: struct ListViewModel<Item: ListItem> the resulting code functions exactly the same and there are no differences in how Swift compiles either notation.

In summary

In this blog post, you learned where the need for generics come from by looking at type safety in Swift and how Array makes sure it only contains items of a single type. You created a wrapper around Array to experiment with generics and saw that generics are placeholders for types that are filled in at a later time. Next, you saw that functions can also have generic parameters and that they can be constrained to limit the types that can be used to fill in the generic.

To tie it all together you saw how you can use generics and generic constraints to clean up some duplicated view model code that you actually might have in your projects.

All in all, generics are not easy. It’s okay if you have to come back to this post every now and then to refresh your memory. Eventually, you’ll get the hang of it! If you have questions, remarks or just want to reach out to me, you can find me on Twitter.

💖 💪 🙅 🚩
donnywals
Donny Wals

Posted on December 5, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related