Remove associated type requirements by moving from protocols to structs

A few years ago, I wrote about a simple technique to encourage code reuse in data sources. I used this all the time in Objective-C, but I could never get it to quite feel right in Swift. I always had to resort to using type erasers or simply forgo strong types altogether.

Luckily for me, seeing Rob Napier’s talk at Swift by Midwest, then watching Brandon Williams’ talk from AppBuilders, I was finally armed with the knowledge I needed to get this stuff working.

In this article, I want to show you how I fixed this problem. We’ll look at:

  • Replacing PATs (protocols with associated types) with structs
  • Using extensions and generic constraints to cover your use cases

If you’ve been struggling to model something using protocols with associated types, or you simply want an easy way to build your data sources, this article is for you!

Where we started

I had gotten used to breaking up my data sources into 2 pieces: one that dealt with the providing data, and one that dealt with presentation.

Broken up data source

This allowed me to reuse the “data provider” part across multiple applications, implementing only the bare minimum amount of code to get a new table view or collection view up and running. What’s more, this was a data source that acted like a data source. No crazy UITableView subclasses, no special controllers. Just vanilla UIKit.

When translating this into Swift, I naturally turned to protocols. I ended up with something like this below.

protocol DataProvider {
    associatedType Item

    func numberOfSections() -> Int
    func numberOfItems(inSection section: Int) -> Int
    func item(atIndexPath indexPath: IndexPath) -> Item
    func indexPath(forItem item: Item) -> IndexPath
}

protocol TableViewPresenter {
    associatedType Item

    func register(tableView: UITableView)
    func cell(forItem item: Item, atIndexPath: IndexPath, inTableView tableView: UITableView) -> UITableViewCell
}

This doesn’t look too complicated, but when you try to build an adapter class that actually uses these, things fall apart rather quickly. Since our two protocols have an associated type, there’s no way build a reusable adapter that holds references to these objects without using a type eraser.

And up until recently, I did just that: I used a type eraser in order to get this to work, but that was a bit cumbersome. It never really felt right. It forced me to write things like this below.

let provider = ArrayDataProvider(with: projects)
let anyProvider = AnyDataProvider(provider: provider) //Type eraser that adds no value
let presenter = ProjectsTableViewDataPresenter()
let anyPresenter = AnyTableViewDataPresenter(presenter: presenter) //Type eraser that adds no value
let adapter = TableViewAdapter(dataProvider: anyProvider, dataPresenter: anyPresenter)

As I mentioned earlier, Rob’s talk at Swift by Midwest made me realize I was on the wrong path, but I didn’t feel like I knew how to fix it.

But then Rob tweeted about Brandon’s AppBuilders talk, and I knew I had to watch it.

This really made things click. Rob made me realize I didn’t want a protocol, and Brandon showed me how to tackle the problem in a systematic way.

(Thanks!)

De-protocolizing your PATs

Here’s how I got myself out of my strongly-type-erased hell.

Instead of futilely trying to get protocols to work, we can instead define a struct with the functions we need:

struct DataProvider<ItemStore, Item> {
    var numberOfSections: (ItemStore) -> Int
    var numberOfItemsInSection: (ItemStore, Int) -> Int
    var itemForIndexPath: (ItemStore, IndexPath) -> Item
    var indexPathForItem: (ItemStore, Item) -> IndexPath?
}

Here’s a quick illustration of how I moved from a protocol to a struct.

Moving from a protocol to a struct

First of all, we can see that all the functions that were defined in the protocol are now properties of my struct.

Second, we notice that each function has an extra parameter. This parameter is the data we’ll be operating on. It replaces whatever implicit input we would be using from our conforming class.

Third, we notice that the type of this first parameter replaces the associated type from our protocol. Instead of dealing with a PAT, we now have a generic struct.

It’s great that we made it this far, but what does this struct mean? What is a data provider, aside from a bunch of function properties?

Here’s how I define it: it’s a type that knows how to separate a given ItemStore into an ordered collection of Item. It knows how to traverse ItemStore using an IndexPath to return an item Item.

How do we actually use this thing?

Now that we have a data structure that makes sense, we can specialize it based on its generic parameters. This is where the technique really shines. Let’s say I want to define a data provider that can map an array to a single section. This is a common scenario, and it’s pretty easy to accomplish:

extension DataProvider where ItemStore == [Item], Item: Equatable {
    static var singleSection: DataProvider {
        return DataProvider(
            numberOfSections: { _ in return 1 },
            numberOfItemsInSection: { data, _ in data.count },
            itemForIndexPath: { data, indexPath in data[indexPath.row] },
            indexPathForItem: { data, item in
                guard let index = data.firstIndex(of: item) else {
                    return nil
                }
                
                return IndexPath(row: index, section: 0)
            }
        )
    }
}

There are a few things going on above, so let’s unpack them:

  • First of all, we’re using an extension with a generic type constraint to produce a default implementation. As long as ItemStore is an array of equatable Item, we’ll have access to this extension.
  • Second, we’re defining a static var that returns a new DataProvider that’s fully configured. We should be able to create a single section data provider in a single line of code.

Using generic type constraints, we can model this data provider rather succinctly.

Let’s try our hand at modelling a multi section data provider driven by an array of arrays:

extension DataProvider where ItemStore == [[Item]], Item: Equatable {
    static var multiSection: DataProvider {
        return DataProvider(
            numberOfSections: { data in
                return data.count
            },
            numberOfItemsInSection: { data, section in
                let subArray = data[section]
                return subArray.count
            },
            itemForIndexPath: { data, indexPath in
                let section = data[indexPath.section]
                return section[indexPath.row]
            },
            indexPathForItem: { data, item in
                for (i, row) in data.enumerate() {
        			if let j = row.indexOf(item) {
            			return IndexPath(row: j, section: i)
			        }
			    }

			    return nil
            }
        )
    }
}

Now that we have a few common cases handled, let’s look at the presentation side.

Table View Presentation

The data part we just looked at can be used with UITableView, UICollectionView, or even UIPickerView, but the presentation half needs to be view specific. Let’s dig into that now.

Using the same technique as above, our struct would look something like this:

struct TableViewDataPresenter<Item> {
    let registerTableView: (UITableView) -> Void
    let cellForRow: (Item, UITableView, IndexPath) -> UITableViewCell
}

This one is a bit easier to manage, and defining it inline is simpler too:

let presenter = TableViewDataPresenter<String>(registerTableView:{ $0.register(UITableViewCell.self, forCellReuseIdentifier: "cell") },
                                           cellForRow: { (item, tableView, indexPath) -> UITableViewCell in
                                                        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
                                                        cell.textLabel?.text = item
                                                        return cell
        })

Notice we’re simply using the default initializer on this one.

Tying it all together

Now that we have a provider implementation and a presenter implementation, we need to create our adapter class. This is the class that actually conforms to UITableViewDataSource.

Its implementation is rather simple: it forwards methods to the provider and the presenter in order to make things work. It’s generic over ItemStore and Item, ensuring that the generic types between the DataProvider and the TableViewDataPresenter line up.

class TableViewAdapter<ItemStore, Item>: NSObject, UITableViewDataSource {
    var store: ItemStore?
    
    let dataProvider: DataProvider<ItemStore, Item>
    let dataPresenter: TableViewDataPresenter<Item>
    let fetchable: Fetchable<ItemStore>

	// and all the UITableViewDataSource stuff. You can check it out
        // at the github link below!
}

You’ll also notice that there’s a Fetchable type. I’ve added this in my own implementation because I want to be able to fetch my data asynchronously if necessary. (Though that’s a bit beyond the scope of this article. If you want to learn about the pattern I was trying to achive, check out the first article from a few years ago.)

Using a data source

Finally, when you’re ready to create your data source, you end up with something like this:

let data = ["Optimus", "Bumblebee", "Megatron", "Starscream"]
let provider = DataProvider<[String], String>.singleSection
let presenter = TableDataPresenter<String>(registerTableView: { $0.register(UITableViewCell.self, forCellReuseIdentifier: "cell") },
                                                      cellForRow: { (item, tableView, indexPath) -> UITableViewCell in
                                                        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
                                                        cell.textLabel?.text = item
                                                        return cell
        })
let dataSource = TableViewAdapter(dataProvider: provider, dataPresenter: presenter, data: data)

And your table view controller’s viewDidLoad can look something like this:

class TransformersTableViewController: UITableViewController {
    func viewDidLoad() {
        dataSource.register(tableView: tableView)
        tableView.dataSource = dataSource
        
        dataSource?.loadData {
            tableView.reloadData()
        }
    }
}

Isn’t that just beautiful? A vanilla data source, modular and strongly typed.

Is it worth the trouble?

Now that may have seemed like a lot of code for something relatively simple. I’m sure you’ve implemented a data source dozens of times.

And while that may be true, it’s worth noting that many of these pieces are reusable. Our concrete data providers, like .singleSection and .multiSection, can be used again and again.

The adapters as well (CollectionViewDataAdapter and TableViewDataAdapter) are also reusable. For the vast majority of situations, you shouldn’t have to make any changes to them.

If you’re creating a new controller, the only piece you’ll have to reimplement is the “data presenter” part of this trio, which also happens to be the simplest piece of all.

All this to say that I find this approach lands right in the sweet spot of reusability. It doesn’t stray far enough from UIKit to seem completely foreign, it strikes a good balance between flexibility and modularity. The base components are useful in many situations, but if you need something special, you don’t need to be a rocket scentist to build what you need on your own.

Thanks again to Rob and Brandon for their amazing talks, and a friend at Big Fruit who helped me work through this problem. Talks are linked below:

Rob’s talk: Swift Generics, it isn’t supposed to hurt Brandon’s talk: Protocol Witnesses

I’ve landed somewhere I’m happy with, and I’ve created a gist of the implementation in case you’d like to see it for yourself.

Take me to the code!