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 UITableView and UICollectionView.
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:
then your ContactCell has Contact as associated-type via the implementation of a protocol (DeclarativeCell):
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.
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 adapter.on.
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:
Other context’s data are path ( 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:
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.
Did you like it?
The new version is available as FlowKit and you can download it from GitHub. FlowKit works with UITableView and 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.
Working with #UITableView and #UICollectionView with type-safe declarative approach. An update version of my previous article. Check it out and tell me what you think 🚀 #swift #iosdev #swiftlang https://t.co/LQtKrVLhN3
Tables & Collections with type-safe declarative approach was originally published in iOS App Development on Medium, where people are continuing the conversation by highlighting and responding to this story.
Powered by WPeMatico