This is the first part of a series of articles on the
ReactiveDataDisplayManager (RDDM) library. In this article, I will describe the common problems that I have to deal with when working with “regular” tables, and also give a description of RDDM.

Problem 1. UITableViewDataSource
For starters, forget about the allocation of responsibilities, reuse and other cool words. Let's look at the usual work with tables:
class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... }
We will analyze the most common option. What do we need to implement? That's right, 3
UITableViewDataSource
methods are usually implemented:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
For now, we will not pay attention to auxiliary methods (
numberOfSection
, etc.) and consider the most interesting one -
func tableView(tableView: UITableView, indexPath: IndexPath)
Suppose we want to fill in a table with cells with a description of the products, then our method will look like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell }
Excellent, it’s not difficult. Now, suppose we have several types of cells, for example, three:
- Products
- List of shares;
- Advertising.
For simplicity of the example, we get the cell to
getCell
method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } }
Somehow a lot of code. Imagine that we want to make up the settings screen. What is going to be there?
- A cell cap with an avatar;
- A set of cells with transitions "in depth";
- Cells with switches (for example, enable / disable input by pin code);
- Cells with information (for example, a cell on which there will be a phone, email, whatever);
- Personal offers.
Moreover, the order is set. A great method will turn out ...
And now another situation - there is an input form. On the input form, a bunch of identical cells, each of which is responsible for a specific field in the data model. For example, the cell to enter the phone is responsible for the phone and so on.
Everything is simple, but there is one “BUT". In this case, you still have to paint different cases, because you need to update the necessary fields.
You can continue to fantasize and imagine Backend Driven Design, in which we receive 6 different types of input fields, and depending on the state of the fields (visibility, input type, validation, default value, and so on) the cells change so much that their cannot lead to a single interface. In this case, this method will look very unpleasant. Even if you decompose the configuration into different methods.
By the way, after that, imagine what your code will look like if you want to add / remove cells as you work. It will not look very nice due to the fact that we will have to independently monitor the consistency of the data stored in the
ViewController
and the number of cells.
Problems:
- If there are cells of different types, then the code becomes noodle-like;
- There are many problems with handling events from cells;
- Ugly code in case you need to change the state of the table.
Problem 2. MindSet
The time for cool words has not come yet.
Let's look at how the application works, or rather, how the data appears on the screen. We always present this process in sequence. Well, more or less:
- Get data from the network;
- Handle;
- Display this data on the screen.
But is it really so? No! In fact, we do this:
- Get data from the network;
- Handle;
- Save inside ViewController model;
- Something causes a screen refresh;
- The saved model is converted to cells;
- Data is displayed on the screen.
In addition to quantity, there are still differences. First, we no longer output data; it is output. Secondly, there is a logical gap in the data processing process, the model is saved and the process ends there. Then something happens and another process starts. Thus, we obviously do not add elements to the screen, but only save them (which, by the way, is also fraught) on demand.
And remember about
UITableViewDelegate
, it also contains methods for determining the height of cells. Usually
automaticDimension
enough, but sometimes this is not enough and you need to set the height yourself (for example, in the case of animations or for headers)
Then we generally share the cell settings, the part with the height configuration is in another method.
Problems:
- The explicit connection between data processing and its display on the UI is lost;
- Cell configuration breaks into different parts.
Idea
The listed problems on complex screens cause a headache and a sharp desire to go for tea.
Firstly, I do not want to constantly implement delegate methods. The obvious solution is to create an object that will implement it. Next we will do something like:
let displayManager = DisplayManager(self.tableView)
Fine. Now you need the object to be able to work with any cells, while the configuration of these cells needs to be moved somewhere else.
If we put the configuration in a separate object, then we encapsulate (it's time for smart words) the configuration in one place. In this same place, we can take out the logic for formatting data (for example, changing the date format, string concatenation, etc.). Through the same object, we can subscribe to events in the cell.
In this case, we will have an object that has two different interfaces:
- The
UITableView
instance spawn interface is for our DisplayManager. - Initialization, subscription and configuration interface - for Presenter or ViewController.
We call this object a generator. Then our generator for the table is a cell, and for everything else - a way to present data on a UI and process events.
And since the configuration is now encapsulated by the generator, and the generator itself is a cell, we can solve a lot of problems. Including those listed above.
Implementation
public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) }
With such implementations, we can make the default implementation:
public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift">
I will give an example of a small generator:
final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } }
Here we hid both the configuration and the subscriptions. Note that now we have a place where we can encapsulate the state (because it is impossible to encapsulate the state in the cell because it is reused by the table). And they also got the opportunity to change data in the cell "on the fly."
Pay attention to
self.cell = view
. We remembered the cell and now we can update the data without reloading this cell. This is a useful feature.
But I was distracted. Since any cell can be represented by a generator, we can make the interface of our DisplayManager a little more beautiful.
public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() }
This is actually not all. We can insert generators in the right places or delete them.
By the way, inserting a cell after a specific cell can be damn useful. Especially if we gradually load the data (for example, the user entered the TIN, we loaded the TIN information and displayed it by adding several new cells after the TIN field).
Total
How cell work will now look:
class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } }
Or here:
class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } }
We can control the order of adding elements and, at the same time, the connection between data processing and adding them to the UI is not lost. Thus, in simple cases, we have simple code. In difficult cases, the code does not turn into pasta and at the same time looks passable. A declarative interface for working with tables has appeared and now we encapsulate the configuration of cells, which in itself allows us to reuse cells along with configurations between different screens.
Pros of using RDDM:
- Encapsulation of cell configuration;
- Reducing code duplication by encapsulating work from collections to the adapter;
- The selection of an adapter object that encapsulates the specific logic of working with collections;
- The code becomes more obvious and easier to read;
- The amount of code that needs to be written to add a table is reduced;
- The process of processing events from cells is simplified.
Sources
here .
Thanks for your attention!