SwiftUI is new, and the Swift language is very powerful. But the newness of the framework and the extensibility of the language has lead me into some deep rabbit holes. I’m not alone in this and I think @ishabazz went down a rabbit hole I had been down previously when I saw this thread on Twitter. I didn’t feel like I knew exactly why I’ve hit this issue before and how I could avoid it consistently in the future.
Let’s start simple to lay the SwiftUI groundwork and then work our way to the problem
We have a list of strings that conform to Identifiable and our SwiftUI can render it easily. The types are homogenious, of the same type, but oftentimes our data comes to us of diverse types or heterogeneous. We should plan for this eventuality ahead of time. For example product teams often want to add a new feature to the top or bottom of a list that is retrieved from at a different API endpoint. So let’s look at what some Swift programmers will want to do with SwiftUI. Start with a protocol! In our case Nameable. Lets define the attributes of a similar model we would like to display and the two models conform to that Nameable protocol. We should be able to render that!
The code seems all well in good in a gist, It’s understandable that we want to have two types that both conform to a common protocol, which (we hope) provides an interface for SwiftUI to render their content into.
But as we press the play button in the playground we find that this won’t compile. Even though we can reason about it, the compiler does not.
Now it’s time to take a break from reading my code and get to our required reading for understanding this problem. Rob Napier has a great talk on Generic Swift. You can follow him on twitter and subscribe to his blog. I really recommend you do both if you’ve found this subject interesting enough to read to this point.
Our issue comes down to a trick we’ve used with NSArray for years, defining an NSArray of NSObjects that conform to an Objective -C Protocol and only using the interface defined by that protocol when we access the items in that array. I used this a lot in the past and it made my intent clear that the array was only for using the parts of an NSObject that are in that protocol. However in Swift we have different types than Obj-C and we enforce those types with vigors. In our case we have an Array Type, that must be filled with concrete objects. Swift protocols are not concrete and therefore we have the first error.
The next error restates the issue from the perspective of the array being passed to the List must have elements that conform to Hashable. While we think to ourselves that yeah they both do, we are missing the issue again with how an Array works. An Array in swift must be of filled with concrete objects of the same type. Protocols are not concrete and therefore we cannot create this array.
We are using the above interface for a List and we can see that SelectionValue must be Hashable, but again it is of one type SelectionValue. So one concrete type must be the contents of the array that we pass into this initializer.
Even though they both look alike, have the same properties, and probably consist of the exact same raw assembly code the Swift compiler will not allow you to mix and match them when put into a Swift Array. Under the hood Swift is making two different interfaces, one for Name and one for Surname of different types.
If you want to know why I’ll direct you back to Rob Napier’s talk on Generics. Its great and he gets into the issue of Swift protocols within Arrays, and it is to do with Generics. The Swift Array uses generics to implements algorithms that manipulate a data structure of the same type. In short if you want to put something in an Array don’t try to be tricky with Protocols with associated Types and generics. Instead you need to keep it simple.
In my case I went back to the tried and true of just writing some more code than I thought I should to get my point across. And I started not with a Protocol but instead a concrete type. I created a third struct that would display the names and give us a Heterogenous collection.
This looks pretty tedious, two types with the exact same interface, just by a different name requiring a third type just to wrap them to be displayed in a List. As those types scale we need another initializer for the Holder. We should realize that this could be error prone.
But generics can help us in this case to remove the tedious initializers. So long as an object conforms to Nameable, we can create initializers of NameHolder for all of them. Now the swift compiler is our tedious code writing templating engine.
The take away from Rob’s talk that I’m using is that generics are for algorithms. And in this case my algorithm is simple, take the known attributes “name” and put it into the holder. If I add a third type that I want to put into the list, MiddleName for example, I don’t need to write a new initializer, unless I want to, which is also perfectly fine.
So there is my deep dive into a very simple problem, a problem that I was able to solve one way with Obj-C because of its protocols. But needed to use a different way, with Swift and SwiftUI. I think its much simpler, leverages the type system in a new way to me and should lead to clear code in your projects as well.