Tables & Collections with type-safe declarative approach
How to forgot DataSource/Delegate in your controllers
This article is also available on my blog.
Not so long ago I’ve published an article called “Forget datasource & delegate: a new approach to UITableView“; the title may appear as a bit pretentious but, believe me when I say I’m tired to fill-up datasource and delegates methods placeholders just to make a simple (or complex) table/collection.
I do not want to bore you with yet another article about massive view controllers but the whole thing is easy to become a big jumble when you want to implement some business logic and flexible rendering to represented data.
My first attempt to approach this problem ended in Flow, a little but useful utility which allows you to create and manage the content of a table in a declarative way. I liked it so much and I’m still using it in different production projects with success; however, as often happens in this work, time, attempts, failures and, in a single word, experiences helped to better understand the problem, the implications of found solutions by giving the opportunity to improve the original work.
The following article recounts the story behind the original idea and propose a new improved approach you can use both for
The original idea
The basic idea behind Flow is to put a single row in relation with both its represented entity (the model) and the associated view (the cell).
To add a new row in a table you need to create a
Row instance linked to its cell class via generics; for its part the cell is strongly linked to the model via associated-type (this because typically you want to use a cell to represent only a single specific data model).
Let me show a small example.
Suppose you need to represent a contact in a cell.
You have your model
Contact as associated-type via the implementation of a protocol (
configure() function will receive a type-safe instance of the model giving the opportunity to prepare the UI with model’s data.
Now you are ready to create your row:
Pretty simple uh? What about an array of contacts? Theoretically you should need to create a counterpart array of Rows, one for each contact, setup the actions and add them to the manager.
Flow provide a shortcut to avoid this kind of cycle:
Did you catch the weird thing?
In case of homogeneous models (as is it often happens for this kind of data representation) you will end up having an array of rows they will almost certainly contains the same actions/configuration repeated over and over again (even if it’s hidden by the shortcut above it still here).
This is the biggest concern about this approach; for a n-sized array of models you will have an n-sized array of Rows which is, basically, unnecessary and wasteful.
Introducing the Adapter
My new approach to the problem involve the introduction of a intermediate object called Adapter. The main goal of the Adapter is to provide a new way to link the Model with the View: for a single Model type you will be able to specify its View representation inside the table (as side effect this results in a weak relationship between Model and View/Cell which allows you to use the same view to represent different models).
In fact the adapter act as central entry point to manage content and events of a cell linked to a specific model. Let me show to you how it differs from the previous approach:
With the code above you are just saying “I want to use ContactCell cell to represent any data inside this table which is of type Contact”.
You don’t need to create a
Row, just add the model you want to represent:
What about events and configuration of the cell instance? Its even simpler, you need to just hooks them to the adapter: each adapter offers a series of event starting from
So, for example you can configure the cell just with few lines of code:
ctx object is what I called
Context; it provides access to the relevant data in type safe manner:
ctx.modelreturn current model (in our case it’s our
ctx.cellreturn the cell instance (in our case a
Other context’s data are
IndexPath ) and table/collection with the reference to the parent’s scrollview. With type safe style Swift compiler can help you with autocompletion and you don’t need to cast objects by your own.
All standard tables/collections events are mapped; the following example you can intercept tap as follows:
Obviously your table may have heterogeneous data; for example you may want some
ContactGroup model instances which needs to be rendered by a
GroupCell . In this case you need just register another adapter and provide your own behaviour.
Once the adapter is registered you’ll be free to add instances of your model everywhere into the table/collection; it’s up to the director to pick right adapter and pass to it the context of the data (path, cell and instances).
The following code create two sections, one from groups array and another from peoples array, then show it into the table:
It’s more compact than delegate/datasource, still perform great and it can be maintained easily.
This is really flexible because of:
- you have not a strong relationship between cell and models; due the fact the cell is free from associated-type you may eventually use it to render multiple models.
- adapters can be reused; while a director instance manages a single table/collection, an adapter manage a pair of data (model/view) and you can reuse it to render its content everywhere in your program.
- there is not a Row entity anymore, so there is not a counterpart array for each model you add to the table (aka we have saved memory space).
- as it happens often you may want to have the same behaviour for a an homogeneous group of models; with adapter you have a single entry point to describe what an event must trigger for a class of objects (and eventually you can act more granularly by discerning the action to perform by looking at single instances. While the first approach is good when your table contains an heterogeneous set of data, this approach unify the best of two worlds.
Bonus track: automatic animations
With this good solution in hands I’ve moved further to simplify some other boring stuff of the table/collection management. One of these is the
reloadData() with animated changes.
Typically in order to animate your table/collection changes you must to keep in sync rendered UI and datasource; this is done via appropriate calls to animation functions (
insertRows,deleteRows,moveSection and so on) which is made inside balanced
beginEdits/endEdits calls (for table) or
performBatchUpdates (for collections).
This operation is a bit tedious and fragile; sometimes — especially when changes are lots — is hard to maintain the consistency (and the order) of the operations without falling in a series of strange errors.
The solution is to get the changes inside the data source before & after a session, then make the appropriate calls to animate them in UI.
There are several algorithms to get changes in a datasource and Khoa done an eccelent work implementing its own DeepDiff project using Heckel algorithm, a technique for isolating differences between files which runs in linear time.
I’ve used his work in order to provide a stable and performant implementation of this feature; the obvious premise is the conformance of your model to the Hashable protocol.
The code above initialize a new editing-session where we add a new sections, remove the first one and swap a model into the last one, all in declarative way.
The last line specify the animations you want to perform for each kind of operation (its just a struct which defines a
UITableViewRowAnimation for insert/remove/move of sections and items (default implementation set all them as automatic).
All the boring stuff are done for you, automatically! As you have seen is very easy to make changes in tables/collections without worrying about paths or casts.
I want that!
Did you like it?
The new version is available as FlowKit and you can download it from GitHub. FlowKit works with
UICollectionView and it also support self sized cell configuration via AutoLayout.
Full documentation is included inside the GitHub project.
I love to hear from you what do you think about it.
Drop me a tweet here or an email; I plan to support and extend it to include features like empty data-set placeholders, pull to refresh and other fancy things.
A special thanks to my colleague Alessandro “Grat” Tonchei for his contribution to design this unique approach.