magnuskahr

writing code

One UITableView, multiple DataSources

Recently when I needed to update my app Styr på Point (eng: To Count Points) I took a hard look at my viewcontrollers, and found that it contained a lot of logic. Way too much. I needed somehow to extract all this out, and began to look at the Command Coordinator Pattern - if you wanna look into this pattern, I really enjoyed "An iOS Coordinator Pattern” by Will Townsend and "iOS Architecture: MVVM-C, Coordinators” by Daniel Lozano Valdés.

By using coordinators, my viewcontrollers got somewhat cleaner, now they didn’t handle the flow in the app anymore, and they could be used more as a plug-n-play service. Do this. But still, my UITableView had three different states: “Show all transactions”, “Show summary”, “Show winner” - and all of this was in one UITableViewDataSource implementation in the viewcontroller.

I then created a marker protocol:

protocol DataSource: class, GameObserver, UITableViewDataSource {
}

The class is because we only wanna allow this to be implemented on classes since we use UITableViewDataSource, and GameObserver, is a protocol from my game-framework.

Next, a simple implementation of the DataSource protocol:

class ChronologicalDataSource: NSObject, DataSource {
    private var transactions = [Transaction]()
    init(transactions: [Transaction]) {
        self.transactions = transactions
    }
}

Where the implementations of GameObserver and UITableViewDataSource was in two different extensions:

extension ChronologicalDataSource: GameObserver {
    func didExecuteTransaction(for game: GameID, transaction: Transaction) {
        transactions.insert(transaction, at: 0)
    }
}

extension ChronologicalDataSource: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return transactions.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let transaction = transactions[indexPath.row]
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "LogCell", for: indexPath) as? LogCell else {
            return UITableViewCell()
        }
        cell.setup(from: transaction)
        return cell
    }
}

Doing this, I ended up with three implementations of DataSource:

  • ChronologicalDataSource.swift
  • HighscoreDataSource.swift
  • WinnerDataSource.swift

And by letting them being a GameObserver, they could subscribe to new data - if you haven’t worked with the Observer Pattern before, go Google it. It's awesome. I then thought that this was good, and I would now just have a reference to all three datasources in my viewcontroller and change between them when needed. But, it really was still too much logic for my viewcontroller to contain, so I made I fourth implementation of DataSource and called it AlternatingGameDataSource. This would now be the datasource my uitableview was linked to and it would contain all the other three datasources and switch between them as needed, by setting one of them as the current datasource. This approach is called the State Pattern.

In the AlternatingGameDataSource implementation of the UITableViewDataSource part, of would simply ask the current datasource for its data:

extension AlternatingGameDataSource: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return currentDataSource.numberOfSections?(in: tableView) ?? 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return currentDataSource.tableView(tableView, numberOfRowsInSection: section)
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return currentDataSource.tableView(tableView, cellForRowAt: indexPath)
    }
}

Now all the logic was contained in defined classes, and my viewcontroller became a lot cleaner.