Creating A Generic List For Complex Collection Views
UITableView
s and UICollectionView
s are great for showing reusable scrolling content, but their delegate based IndexPath
api can make them hard to deal with for more complicated screens. You end up spreading presentation logic for a single section across a number of functions making it hard to get a clear picture of how a section will behave by just look at the code. Ideally all logic for a section would live together, and for bonus points we could not have to deal with IndexPath
s.
At ibotta we have large collection views with a lot of different types of cells / sections. On top of the rendering complexity that brings we also needed to support skeleton load and incremental loading. To accommodate these requirements while keeping our code clean, we created a new generalized List that could be configured and used on all of our screens.
Along with showing the right content we also need to correctly handle cell selection, cell updates (diffing), impression tracking, and prefetching. As the number of sections in a collection view grows, it becomes clear that a new layer (or two) of abstraction is needed to handle all of this complexity.
⚠️ This article assumes you are comfortable with Swift and generics.
Composable Sections
We are going to think of a collection view as a number of small autonomous sections. We might have a section for a header, and then a horizontal section for retailers, and then a section representing a grid of offers. We will represent each of these sections as a SectionPresenter
, but because we want to use this on a number of screens we will keep our SectionPresenter
s abstract and configurable; handling things like index paths under the hood.
SectionPresenter
s are going to be generic classes that hold all of the logic needed to render cells and handle actions, but they will be independent of their data source. We are going to employ an MVVM-like architecture where a larger Properties
struct is passed to a SectionPresenter
and converted to a single cell's ViewProperties
; SectionPresenter
will be generic over the Properties
and ViewProperties
. To keep tracking of the loading state of our data we will use a LoadableProperty
enum. All together this will look something like:
Notice how SectionPresenter
encapsulates the lookup logic to convert Properties
to each section's appropriate ViewProperties
. Because ViewProperties
are a loadable array we can infer a lot of information about loading state, and use the array indexes as collection view indexes. This separation of section logic from our live data is key. We can use our SectionPresenter
like:
Now that we have this configurable foundation in place we can start to build out SectionPresenter
. We can add simple rendering methods like:
We’ve just given our SectionPresenter
several closures that can be configured by consumers. We will use this same pattern to add all functionality to our SectionPresenter. Notice how some things like cell count are automatically handled, and the logic to show the loading cell or the data cell is consolidated to one class that will be reused everywhere. We will go over cell reuse later in this article. We can create and configure our a SectionPresenter
like:
We would like to represent collection view sections with SectionPresenter
s, but because each section presenter will have different generic types for Property
a type erasure is needed. To do this we will need to put SectionPresenter<Properties, Property>
behind a protocol and create an AnySectionPresenter<Properties>
class. There are a lot of ways to create type erasures in swift; we are going to use a simple closure based erasure. John Sundell has a great article on type erasures if you want to learn more. This can be done pretty easily like:
Now we can represent all of our sections as [AnySectionPresenter<Properties>]
. Each SectionPresenter
can be generic over a specialized subset of properties, but we only need to worry about the larger Properties
struct in our List.
List
We now have an array of sections, but we still need to build out the logic to render content. This becomes relatively straightforward; we are going to treat each SectionPresenter
as a single section in our collection view. We are going to use a UICollectionView
as the underlying mechanism to show our content. Apple has done a lot of the heavy lifting with UICollectionView
and there is no need to reinvent the wheel. However our List api will be mostly UICollectionView
agnostic, so switching to a new underlying view type (or even swiftUI) would be simple enough.
To get started our List
class will need to maintain an array of AnySectionPresenter
and the current state of Properties
. Any time properties or sections change we will want to update our List
.
Notice how List
is generic over Properties
, this will allow everything to work in a nice type safe manner without any dangerous implicitly unwrapped optionals.
Now we only need to write general rendering code in List
because all the complex use case specific logic lives inside our SectionPresenter
s. We can enable cell rendering like:
Because we’ve separated out rendering logic from live data while still keeping everything generic our cell rendering methods get type safe ViewProperties
. We've also consolidated the IndexPath
logic to our List
and SectionPresenter
in a way that no public consumers will have to know about it. And we've enabled complexities like incremental loading with minimal effort by treating each section as an autonomous unit.
The final usage of our List
will now looks something like:
Cell Reuse
For UICollectionView
s to work properly we have to register reusable cells, and then dequeue cells by identifier. To do this we are going to create a new AnyCellProvider
class to allow type safe cell registration and rendering from view properties. This will look like:
You can see the initializer takes a cell type and a render method, then it builds closures to be consumed by the SectionPresenter
. This has the effect of type erasing the cell type. The render method can be used by public consumers to decorate and configure the cell. The Property
of our AnyCellProvider
will match the Property
of our SectionPresenter
allowing cells to be creating and rendered using type safe properties.
We can now adjust our SectionPresenter
's configurable cell closures to use an AnyCellProvider
.
We also need to adjust our AnySectionPresenter
to use the new signatures for cell and loadingCell, but that should be straight forward so we aren't going to get into it.
Using a SectionPresenter
with an AnyCellProvider
looks like:
Because SectionPresenter.cell
is expecting a variable of type AnyCellProvider<Property>
, xcode is able to infer the Property
type for AnyCellProvider
. While this example only supports sections with a single cell type, we could expand our AnyCellProvider
implementation to handle multiple cell types per SectionPresenter
. This rendering pattern provides a clean and extensible interface for complex rendering requirements.
Conclusion
So far we’ve created a simple List
class that can be used to show lots of heterogeneous content, but this is only the beginning of what can be done with this pattern. We can add all sorts of additional functionality to our SectionPresenter
and List
like: selection callbacks, prefetch logic, impression tracking, and smarter updating using item level diffing.
As an added bonus all of the functions inside a SectionPresenter
should be pure functions, so you can easily test a SectionPresenter
's presentation logic.