In iOS development, the core of nearly every app rests on the foundations provided by
UITableView. These APIs make it simple to build interfaces that display the data in our app, and allow us to easily interact with those data. Because they are so frequently used, it makes sense to optimize and refine how we use them — to reduce the boilerplate involved in setting them up, to make them testable, and more. With Swift, we have new ways with which we can approach these APIs and reimagine how we use them to build apps.
The common problem
Setting up a table view or collection view has always required a lot of boilerplate — re-implementing the UITableViewDataSource and UICollectionViewDataSource protocols time and time again. There are strategies to abstract these protocols into separate data source objects, but until Swift they either had to be specialized for a specific type and thus not reusable, or reusable and not type-safe. Further, these protocols intermingle different responsibilities. As Ash Furrow points out, it would be better to have many single-purpose protocols, instead of having one protocol that does everything.
A modern solution
One of the first things I built with Swift (and just recently updated) was JSQDataSourcesKit because I wanted to address these issues. Inspired by Andy Matuschak’s gist, the goals of the framework are the following:
- Remove the data source protocol boilerplate.
- Be data driven. That is, if you want to change your view then change your data or its structure.
- SOLID, with a focus on single responsibility, interface segregation, and composition.
- Pure Swift (no
@objc, when possible)
The framework is crafted of four main types of components:
DataSourceProvider. For each component, there is a corresponding protocol or type to use with both table views and collection views.
SectionInfo— Section objects contain an array of your models. They represent one section of data. The framework provides
TableViewSectionstructs. However, they conform to the
TableViewSectionInfoprotocols, respectively. This allows you to build your own.
CellFactory— Cell factory objects are responsible for creating and configuring a cell for a given model. There are
TableViewCellFactorystructs, which conform to the
TableViewCellFactoryTypeprotocols, respectively. Again, this design allows you to build your own cell factory objects if you do not want to use the ones that the framework provides. For collection views only, there’s also
CollectionSupplementaryViewFactorywhich works similarly.
BridgedDataSource— These are actually opaque objects. They are
privateto the framework and not used by clients directly. Bridged data source objects implement the actual
UITableViewDataSourceprotocols. The name refers to the fact that these objects are bridging the data source protocol methods from Objective-C classes (i.e.,
NSObjectsubclasses) to pure Swift classes. In order to implement
UITableViewDataSource, a class must also implement
NSObjectProtocol, which essentially means inheriting from
NSObject. As mentioned above, I want to avoid the baggage of
NSObjectfrom dragging its dirty fingers through the rest of my types, so it is all contained here.
DataSourceProvider— Data source provider objects are composed of an array of sections, a cell factory, and a bridged data source. (And for collection views, there’s also a supplementary view factory.) A provider object orchestrates and mediates the communications between its constituent parts, which know nothing about each other. Finally, as the name suggests, it provides the data source for a table view or a collection view, which happens via its private bridged data source instance. To clients, it looks like this:
Putting it all together
Let’s take a look at how this works in practice. Here’s an example for a simple collection view.
First, we populate our section objects with our models. Then we create our cell and header view factories. Finally, we pass all of these instances to our data source provider. That’s all. The collection view will now display all of our data. The result is an elegant, composed, protocol-oriented, and testable system. You can independently test your models, test that each factory returns correctly configured views, and test that the
provider.dataSource accurately responds to the
UICollectionViewDataSource methods. Using this framework with table views follows similarly, with the main exception being that table views do not have supplementary views.
Also remember that the
CollectionViewDataSourceProvider only speaks to protocols — not the concrete objects used in the example above. Its signature is the following.
Do not be afraid! Before Brent Simmons accuses me of contributing to angle-bracket-T blindness, let me explain. There are three generic type parameters. We specify that these three types must conform to the
CollectionSupplementaryViewFactoryType protocols. Finally, the
where clause specifies that each object must deal with the same kind of model objects (
Item). This prevents us from trying to use a section of
ModelA with a cell factory of
The example above just scratches the surface of what this framework can do. It also integrates with Core Data and NSFetchedResultsController. For this, instead of initializing a
DataSourceProvider with an array of sections, you pass an
NSFetchedResultsController instead. Even more, there are
FetchedResultsDelegateProvider classes that encapsulate all of the tedious boilerplate for
NSFetchedResultsControllerDelegate for table views and collection views. If you want to see more examples, I’ve put together a great example app in the repo that exercises all functionality in the framework. You can find complete documentation here.
I’m looking forward to building apps in Swift with patterns like this, and you should be too! If you have been confused by these hipsters talking about protocol-oriented programming and composition over inheritance, hopefully this serves as a practical example of what they mean. If you are working on an app in Swift, I encourage you try JSQDataSourcesKit. Let me know what you think, and feel free to send me a pull request!