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!"
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"]
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
}
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]
}
}
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"
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>
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
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
}
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
Let’s pick apart the method signature for decode a bit:
-
func decode<T>
: thedecode
method specifies that it uses a generic object calledT
. Again,T
is only a placeholder for whatever the real type will be, just likeElement
andOurElement
were in earlier examples. -
(_ type: T.Type, from data: Data)
: one of the arguments here isT.Type
. This means that we must call this method and specify the type we want to decode data into. In the example, I usedSomeType.self
. When you call the method withSomeType.self
without explicitly specifyingT
, Swift can infer thatT
will now beSomeType
. -
throws -> T
: This bit marksdecode
as throwing and it specifies thatdecode
will returnT
. In the example,T
was inferred to beSomeType
. -
where T : Decodable
: this last bit ofdecode
's method signature applies a constraint toT
. We can makeT
whatever we want, as long as that object conforms toDecodable
. So in our example, we’re only allowed to useSomeType
as the type ofT
ifSomeType
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]
}
}
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]
}
}
Neat, right! And instead of the following code:
let viewModel = FavoritesViewModel()
You can now write:
let viewModel = ListViewModel<Favorite>()
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
}
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 thewhere
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.
Top comments (0)