Creating A Generic List For Complex Collection Views

Alex Pelletier
Building Ibotta
Published in
6 min readMay 18, 2020

UITableViews and UICollectionViews 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.

SectionPresenters 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 SectionPresenters, 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 UICollectionViews 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.

--

--