Reactive Data Display Manager. The great refactoring
In mobile development, screens can rarely do without UITableView or UICollectionView. These UI components help divide screens into cells, configure or substitute them separately if necessary. However, along with that comes a lot of routine tasks to configure DataSource and Delegate collections.
To cut back on routine tasks, we’ve created a library called Reactive Data Display Manager (RDDM). The core of it is an adapter implementing DataSource and Delegate and a generator corresponding to a cell in a collection.
The library has been getting better and more powerful. But the more improvements we were making, the clearer we saw that there was something wrong with the architecture in the library. The final straw was trying to make RDDM interact with UITableViewDiffableDataSource, which ended in a fiasco.
Hi! I’m Nick, an iOS teamlead at Surf. Recently, I’ve been responsible for improvements in RDDM. In this article, I’ll be telling you a story of refactoring that helped rejuvenate the library and make it interact with the latest APIs that handle collections.
Want to stay on top of app development trends and know how to create apps millions of people love?
The struggles behind expansion
RDDM was brought into life by an idea to make UITableViewDelegate and UITableViewDataSource isolated entities so as not to implement their methods. However, in the course of development, we faced the fact that this solution is hard to modify: each modification brings about a new descendant of an adapter.
Let’s take a look at an example screen with paging to get a better understanding. This mechanism is often used when there’s no point in loading the entire body of data: e.g., if there is a lot of data of similar type coming from the server and it can’t possibly all be shown on one screen at once.
Backend developers provide us an API to load data package by package. What an iOS developer has to do in this case is single out the moment when the next package has to be requested. It’s also a good idea to minimize the delay: anticipate user intentions and request data a bit earlier.
To do that, you need a delegate method tableView(_:willDisplay:forRowAt:) that starts executing shortly before the cell is rendered. If you have an index, you can identify that the last cell is ready to be rendered, but when using RDDM, one doesn’t simply override a delegate method. You need to inherit =(
It’s not the smartest decision in the long run, but it’s better than copypasting logic from one controller into another, isn’t it? We’ll use this adapter for screens with pagination. But what if we decide to expand what this adapter is capable of? Then, we’ll have another descendant. If we need to combine the capabilities of these adapters, that would be a descendant’s descendant.
Over the years of using RDDM, we’ve accumulated too many dependents from the basic adapter. Within the nine projects we’ve analyzed, there turned out to be 40 dependents and their combinations of all kinds. That’s intimidating, right? But it’s not the worst part.
With iOS 13 released, Apple created a new type, UITableViewDiffableDataSource, in addition to the standard UITableViewDataSource. We found ourselves in a tough situation: we couldn’t substitute dataSource and try out the new one because the RDDM adapter was a monolith blend of UITableViewDelegate and UITableViewDataSource.
That’s what made us think about refactoring.
Refactoring
The results we were looking for:
- Expanding without dependents.
- Substituting delegate or dataSource.
- At least partial backward compatibility. The iOS team at Surf has twenty developers. All of them use RDDM in production, so it was important to keep the concept of the library and the interface of the adapter intact.
The architecture of the library before refactoring
The structure of the old architecture
This is the general structure for UITableView and UICollectionView. Attachment to a cell or collection is mediated through aliases — CollectionType, CellGeneratorType. Stipple marks the parts that have no issues with expandability or substitution. In this case, only with the adapter interface.
We’ve already discussed the drawbacks to such an architecture above: there are too many dependents on the basic adapter, and the new dataSource type is impossible to use.
What we got after refactoring
The structure of the new architecture
The new architecture ended up having a pretty broad range of entities. To combine them into a specific adapter, we use the pattern called Builder.
Delegate and DataSource are new adapter properties. They can be substituted — granted, you can only do that at initialization, but that’s good enough.
Animator responds to updates in a table or collection. It can be substituted, but in fact it’s enough to have two ready-to-use implementations you can choose depending on the version of your OS. For iOS older than 11, we paste and delete inside beginUpdates/endUpdates. In further versions, we use performBatchUpdates.
For minor modifications, we’ve introduced plugins.
PluginAction provides a reaction to a delegate or source event and interacts with the generator or adapter. For example, we can use it to support paging.
FeaturePlugin is something we use for more complex features that require substituting the returned value in the delegate or source. For example, when you support swipeActions in a table.
Generally, a feature equals a plugin. In other words, when we need to grant an adapter a new capability, first we will think of how to implement it with plugins.
Use cases
Paging
Paging is now dealt with using a plugin. The body of the main method in the plugin speaks for itself. The action plan is equivalent to the one in the descendant adapter. Here, TableEvent is just an enum that describes the events in the delegate or dataSource.
Enabling the plugin is pretty easy
Bonus track: stress test adapter
The number of plugins is only limited by your imagination.
The stress test adapter checks plugins for compatibility. One adapter contains 11 plugins:
- Displayable transfers willDisplay and didEndDisplaying to the generator corresponding to the DisplayableItem type.
- Direction defines the scroll direction. The reaction is forwarded to ScrollEvent — the event in ScrollViewDelegate.
- HeaderIsVisible helps set a reaction to a section header being visible.
- We already know what LastCellIsVisible is.
- Selectable transfers didSelect to the generator corresponding to SelectableItem.
- PrefetchablePlugin adds image prefetching. The Example project has a prefetcher implemented for Nuke, but you can use any other library supporting prefetching to load images.
- Foldable supports foldable cells.
- Movable supports movable cells.
- Refreshable is a plugin supporting UIRefreshControl.
- SwipeActions adds support for sipe menu in tables.
- SectionTitleDisplayable helps display index headers.
11 plugins in a single adapter! Imagine how terrible an inheritance tree would look for an equivalent adapter, if we were writing it in accordance with the old nonexpansible architecture.
All of these plugins have already been implemented in the library: you can use them in your projects if you get updated to RDDM 7.x.
Перевод подписи: Examples of Foldable and Movable in action
Enabling DiffableDataSource
It’s pretty obvious that UITableViewDiffableDataSource can’t be enabled with a plugin. Besides, it’s not a specific protocol but a generic class.
Let’s look at the pros of UITableViewDiffableDataSource in the context of a mutable collection.
More often than not, we receive content from the server and display it unchanged, but sometimes we give users a choice to:
- delete a cell,
- insert a new cell before or after another cell,
- swap cells around.
I’ll be calling such cases “mutable collections”.
Cells can be substituted in one of the following ways:
- manually,
- with a third-party library,
- with a new DiffableDataSource.
Manual method
The manual method suggests following the algorithm:
- identify the index of the element;
- update the array of generators by transferring or deleting the element;
- performed the same operation with indexes in the collection by calling methods insertItems/deleteItems.
In this case, there’s a method called performBatchUpdates hiding inside the animator.
Taken all together, that’s one search operation, two deleting operations, and two inserting operations. That said, you have to delete and insert in the right order for the substitution to end up as expected and not cause the app to crash.
In a way, the RDDM adapter helps follow the right order of operations because they are hidden inside its implementation. But for every new complex operation, you’ll have to accurately state the IndexPaths and call the operations specific to the collection in the right order.
It’s a bit of an effort. That’s why solutions like DifferenceKit came to exist. We’re not going to reinvent the wheel — instead, we’ll make our library interact with a third party one.
DifferenceKit
With DifferenceKit, the substitution algorithm is roughly as follows:
- Make generators differentiable. The library has a dedicated framework for that. It’s similar to Equatable but with an additional field for the unique ID.
- Create a snapshot of the generator array.
- Update the generator array by transferring or deleting an element.
- Create a new snapshot of the array.
- Generate changeSet out of the content state before and after the changes were made using the snapshots.
- Apply the changeSet to the collection, specifying the animations needed.
The library is written via extensions to the collection. It can be used in iOS 9 and younger, i.e., in practically any project.
The algorithm now has more steps, but the code has become clearer.
makeSnapshot transforms an array of sections and generators into a format familiar to DifferenceKit. We take a snapshot once before the changes and once more after the changes.
StagedChangeset provides a structured set of operations like insertItems/deleteItems. Inside the reload, there’s a block called performBatchUpdates: it’s where all the operations from the set will be performed.
reload also has a block parameter called interrupt, where you can specify what set of operations triggers the full reloadData in the collection. All in all, the resulting solution is flexible and fits the concept of RDDM perfectly.
We didn’t put the DifferenceKit inside the RDDM so as not to provide the library with codependencies.
To see how the two libraries coexist, check out the Example project.
DiffableDataSource
Besides the third-party solutions, there’s a system approach from Apple. We really wanted to use them, but there’s one serious constraint: DiffableDataSource is only available on iOS 13 and later. Due to that, we can’t use DiffableDataSource in many of our projects yet. But we’re soon about to start projects with this exact target, so we’ve examined the approach in advance and made it interact with RDDM.
Similar to the previous examples, take a look at what to do to substitute a cell with another cell:
- All generators must be differentiable. SectionIdentifiable and ItemIdentifiable are aliases for Hashable + Equatable.
- Define DiffableDataSource — generic DataSource where all the main methods of the numbersOfSections, cellForRowAt and other types are already defined. To generate cells, headers, and footers, it uses the block that is set at initialization.
- Update the generator array by transferring the element or deleting it.
- Create a snapshot NSDiffableDataSourceSnapshot of the generator array.
- Apply changes.
The resulting code to substitute one cell with another one looks like this:
This code combines all the good parts of the two previous solutions:
- The clarity of the manual solution.
- The handiness of the snapshot approach with DifferenceKit.
The flag animatingDifferences helps choose the animations needed for the cells automatically. The false value is equal to calling reloadData.
As opposed to the solution with DifferenceKit, we only make a snapshot once by creating it from the generator array after the changes are made.
The snapshot of the state before changes can always be found inside dataSource. We take advantage of the fact that we always have the chance to make a snapshot of the generator array and don’t create conflicting states.
All operations should be performed via the adapter. The upside of the RDDM adapter is that it combines a delegate and a data source and helps add features with plugins.
Using DiffableDataSource without RDDM would look a bit different.
A snapshot can be used instead of an adapter. NSDiffableDataSourceSnapshot is not just a snapshot but an array. It’s a more complex class to which you could apply insert/delete operations — much like the ones in the collection. We don’t need this capability, though. In this case, we:
— Either replicate the operations just like in the manual method.
— Or break the correspondence between the states of the adapter and the snapshot, because they are autonomous.
DiffableDataSource is a must have for search screens or forms where the number of cells or their height is often modified, making them hard to manage manually.
If your case matches the target requirements, make sure to try it out.
If you found RDDM interesting and would like to give it a try in your project, you’re most welcome at our repository. Our goal is to keep perfecting this library, and we’re always happy to have more contributors.