Better table view data sources: capturing and communicating state
A practical guide to more robust table and collection views in your native applications
Almost every native UI I’ve built is rooted in Table or Collection Views. I have written about my approach to table views and their data sources which serves as a high level primer.
This post outlines a common problem developers face as they churn out code; the temptation to defer edge case handling to a later date, which never comes. The edge cases I focus on are loading, load error, and no-data but the approach I present below accommodates any you may need.
Not everyone ignores these edge cases, but I’ve noticed that if they are handled, they’re often handled in front of the table view and managed via the lifecycle of the UI (i.e. present view controller, initiate load, show spinning disk, return from DA, hide spinning disk, reload table). I prefer to incorporate my loading/error/no-data UI directly into the table view itself, and more importantly, I want my data access and state capture to be independent of the UI and its lifecycle. This pays dividends when you have table views that are composed of sections dependent on different data sources that may be at different states of loading. It’s often important to present data as it becomes available (section by section), rather than waiting for everything to be available in its entirety. And that’s only possible with inline lifecycle communication.
Table Views are where data access failures and errors silently die
Scenario: You’re out of town with a terrible 3G connection and you are overcome with shame and disgust as you realize how broken your app feels away from an urban Californian high speed LTE connection.
Problem: Communicating network based data loading and enabling failure retry is essential for a professionally built app and UI. Remember, this is a mobile platform with connectivity challenges. If your Table Views are littered with // TODO: handle error, you can improve them by trying the following approach
Approach: Create a standard enumeration of data types within your data source, and have your table view handle each enum case. Here is mine:
As the data source assembles itself by reading from the model layer, it can also ask your data access object(s) about the status of any relevant pending requests. Is something currently loading? Was there an error? What was the error?
If the data source gets any interesting answers to those questions, it can assemble and append a data block to its own internal data hash, with the corresponding type enum value. So let’s say the model layer is empty, and the data access object says that a request is still in flight. The data source could append a loading block, or a load error block with the associated error object.
When the data source is asked for a data block at a given index (path) the table view is then able to quickly switch through all the possible types of block, and respond accordingly. By setting up some simple loading and retry cells app wide, and registering for them to the table view, handling these states becomes boilerplate code. There is also the opportunity to customize each case, but crucially, it gives a quick and consistent way of communicating these often overlooked states: loading, load error (with ability to retry) and no data. The view can also choose to totally ignore those enum values, and the data source — if it’s not dependent on loading for example — can be clear about never using them.
The standard LoadError cell I built needs to be initialized with a Load Error Retry Delegate, which the view controller then dispatches back to the data wrangler.
You can see how the same switch logic we use in cellForRowAtIndexPath can be applied for all other table view delegate and data source protocol implementation methods.
That cell registration in viewDidLoad which enables the nice, standard, simple handling of each state is in a util which looks like this:
Conclusion
I use this pattern everywhere. It’s low maintenance and means that I can focus my development efforts on the product and UI we designed to present to the user in the table I’m building. But it gives me the confidence that on spotty connections or with data access problems, the users of the App I’m building are going to know what is going on (loading) and is going to be empowered to take action (retry on error). There’s never going to be a scenario where they have to force close the app to retry a failed data access, or navigate back and forth to try and re-initiate some data access.
N.B. Don’t use a default handler in your block type switch statements. If I add a new enum value, I want the compiler to alert me to the places I need to add support for a new state, and it helps you build confidence in your coverage of all ways these blocks of data are used. For example, it means you’re likely to consider the tapping of loading cells alongside tapping of custom cells.