Build Complex Lists in SwiftUI
Let’s create a collection-esque view in SwiftUI
Building complex lists in iOS with UICollectionView
has been painful. Before iOS 13, creating a layout with multiple sections, each with different behavior and different axes, usually ended up creating a custom UICollectionViewLayout
. Fortunately, along with iOS 13, developers got UICollectionViewDiffableDataSource
and UICollectionViewCompositionalLayout
. They made it possible to create section layouts in a straightforward way. Linking data to a given section did not require writing a lot of additional code, thanks to the data based on the Hashable
protocol.
SwiftUI is a great framework that will make it easier and faster for developers to create views in the future. Unfortunately, it is currently at an early stage of development, and in my opinion, it lacks many key components. One of them is the equivalent of the well-known UICollectionView
. So I decided to do it on my own.
To better illustrate the mechanism, I will show the use of the collection we’re going to create, with the example of a simple mountain booklet app.
Overview
Assumptions
To know which way to go from the beginning, let’s define the goals:
- API follows the conventions of already existing SwiftUI components.
- The mechanism of data binding with the section layout is based on
Hashable
. - The element responsible for defining the layout of the section works based on the section type, not the data itself.
API design
Since we want to follow the SwiftUI convention, the List
's initialization will use the result builder feature. The code below provides an overview. Thanks to what @ObservedObject
gives us, we don’t have to worry about reloading the view because SwiftUI will do it for us — exactly as in the case of using List
itself.
Implementation
Knowing just as much as we do at this point, I feel like we’ve designed the API quite well. So we can move on to implementation.
Models
What we defined previously stipulates that the Collection
will base on data separated into sections.
All we know is that the sections need to be Hashable
to use the SwiftUI’s ForEach
mechanism. So section protocol looks like this:
As I mentioned earlier, the application will be a booklet of mountains and mountain ranges. Let’s create the section’s structs, to blueprint an overall picture of how the mechanism works. We will use them later to declare the data that will be displayed.
To not overwhelm you with the code, I will not show you the structs used as associated values’ type. I will link the GitHub repo with all sources at the end.
Transformation of data into a view
All right, we set up our models. We force the section models to be Hashable
. We have achieved goal 2!
But how will the data be displayed? The assumption is to collect the components that are responsible for displaying a given section in the closure. Let’s put some work into building the collection with the use of the result builder.
A prerequisite for its creation will be what it is supposed to collect. These components, which we will call CollectionSectionController
s, have to be linked to the section types somehow. As usual, let’s use the protocol to achieve that.
At this point, you’re probably asking yourself what the purpose of ViewType
is. Can’t view(for:)
return a View
? SwiftUI’s View
is a protocol with the associated type of body
. The compiler wouldn’t know what type of body
this view is. In a definition of a section controller, we can erase this type using some
. If you haven’t already, I encourage you to read about opaque result types.
Some of you may see the limitations of this solution — the first results from using result builders. We can’t just define a constructor to take any number of protocols with associated types. This is the same problem that the developers of SwiftUI encountered. When we look at the implementation of ViewBuilder
, we see the following:

Again, the compiler would not be able to determine the types of arguments. Therefore, the collection builder API has to look like this:
Collection guts
We’ve come this far and still haven’t even prototyped the primary component we’ve been working on for so long. Let’s deal with it finally.
The first thing missing is the sections
elements’ type. It cannot be a CollectionSection
because this protocol inherits from Hashable
. By erasing sections to AnyHashable
we will lose the section type. Thereupon, determining which section controller to query for the view would be impossible.
Thus we need to create a class that will allow us to erase the section type. Simultaneously, it has to store its type to know from which controller to request the view.
The second thing the Collection
is still missing is section controllers that will be prompted for the view. With goal 1 in mind, we designed the CollectionBuilder
to collect section controllers but yet haven’t figured out how to build a component to return. The SwiftUI’s ViewBuilder
uses TupleView
to return a result. We also need to group section controllers somehow. For this purpose, let’s create a Registry
of SectionControllers
.
Unfortunately, this is the second time that an associated value limits us in the protocol. We will handle it exactly as before. Let’s create a class responsible for erasing the type, and then let’s go back to the Registry
. To be able to match the controller to the data, we keep the section type in AnySection
. In this case, we will do the same to then match the controller to a given section.
Since we no longer need to worry about the associated value in the SectionController
, let’s create a Regsitry
. For now, let it be responsible only for gathering the controllers. We will use it as an object that we return in CollectionBuilder
.
At this point, it’s fair to say that we took care of an API of our Collection
. Goal 1 achieved!
Well, so what else is missing? Collection
still does not query section controllers for a view for a given section. According to how we designed the API initially, let’s create a collection initializer using a builder. Thanks to this, the registry will be included in the collection.
As above, we use view(for:)
to query views from Registry
. This method’s still not implemented, so let’s add it.
Earlier, we implemented the restore()
method in AnySection
. Now let’s use it to cast the passed section to the required type, based on the type of section controller. This way, we will query the section controller for a view, knowing the data type. Casting is necessary because a given controller only works for a kind of section.
To not lose information about the section controller type, we will create a closure in the constructor of AnyCollectionSectionController
, returning a view with the erased type.
And now we can use this closure in the Registry
to gather a view from a section controller.
OK, but what about goal 3? We have just achieved it! Our section controllers base on SectionType
. Thanks to this, we can create many sections, and if they are of the same type of data, they will require only one controller.
The last thing we should add is a contract that will force us to pass the collection’s necessary data. So let’s create a CollectionDataSource
. It will only contain the get-only sections
property. However, if you want to extend what we have made somehow, you can extend this protocol.
Content declaration
Let’s move back to the data models we created earlier. It’s a time to use them and define the mountains and mountain ranges that will be displayed. With this, we will check if our API works as expected.
But wait! What is the eraseToAnySection
method? It’s a simple function that extends CollectionSection
protocol. It lets us wrap sections into AnySection
in a much more convenient way.
Laying out the collection
With the configured ViewModel
, which satisfies CollectionDataSource
, we are finally ready to create our Collection
. To build a collection, we need sections. What I like the most is that we declare the view of each section from the controller level. Thanks to this, we can quickly create headers, footers, and other components of each section.
For example, let’s make a controller responsible for generating the view of the mountain ranges section.
With sections created, let’s create our collection. Thanks to @ObserverObject
, whenever the view model is updated, the collection adjusts to the content change.
The configured collection, when run on an iPhone, looks like this:

Conclusion
The whole mechanism we have created today meets three fundamental assumptions. It matches well with other components that come with SwiftUI by default, so applications built with it will be consistent.
Thanks to the fact that we used protocols to define components, we can easily extend the Collection
with new functionalities, such as declaring section order from the model level.
The whole thing is just a core that you should adjust to your own more complex needs. At this point, it is not able to replace the UICollectionView
, but I think it is a big step forward. Nevertheless, I look forward to the SwiftUI team’s ideas and the new functionalities of this robust framework.
Source code as a Swift package with demo app included: https://github.com/kkiermasz/Collection.