Using Diffable Data Sources in Xamarin.iOS

iOS13 includes a large number of exciting new features, some of which I outlined in a previous post. One addition in the release that has received less attention is the new option for UITableView and UICollectionView data source configuration - the 'diffable data source'. The aim of diffable data sources is to greatly simplify the interactions between a dataset and UITableView/UICollectionView when changes occur, by allowing UIKit itself to manage the diffing, reload and animation of sections and cells. As a more opinionated API, this feature bakes in some assumptions that are undesirable from a Xamarin.iOS perspective. However, with the use of a few helper methods and classes - it's possible to make use of it from Xamarin.iOS without too much trouble.

Do I need this in my life?

The answer for many people is probably 'no'. If you're a Xamarin.Forms user, you don't typically deal directly with UITableView and UICollectionView APIs, so this probably wont be of interest. Even if you work 'natively' with Xamarin.iOS, application of the MVVM pattern typically results in the delegation of datasource/control interaction to your MVVM framework of choice via bindings or dedicated helper classes. For example, ReactiveUI includes a ReactiveTableViewSource class that allows you to work with a UITableView in a more MVVM-friendly way, relieving you of the need to deal directly with nasty indexPaths and APIs like BeginUpdates. In this case, again, the addition of diffable data sources probably won't interest you.

If you do build iOS-only applications using Xamarin.iOS, like I do from time-to-time, diffable data sources do present an interesting option for table/collection view configuration.

The old way

If you've ever worked with a UITableView or UICollectionView directly from Xamarin.iOS, you're probably aware that you control the way those controls get data by passing in an instance of a 'data source', typically a subclass of UITableViewDataSource or UICollectionViewDataSource. For simplicity's sake, I'll focus on UITableView from now on, which requires a UITableViewDataSource to implement callback methods that answer three key questions:

  1. How many sections does the data for this table have?
  2. How many rows does a given section in the table have?
  3. What cell should be displayed for a given row in the table?

Essentially, when the tableview is put on screen or reloaded, it calls the first two methods to determine the size of the dataset. Then it calls the third method as and when it needs to display cells on screen. Your implementations typically call to a backing store like a List<T> for things like the counts. When I first started out with Objective C this was a bit of a mind bender for me, but a callback-based configuration can be very flexible.

The challenge with the API then, is handling changes to the dataset. For example, inserting and removing rows, or reordering them. Changes of this nature need to first occur in the backing store (e.g. your List<T>), but then the precise set of changes need to be described to the tableview in order to have it animate the changes nicely. For example, if the second item from the list was removed, and a new one was added at the end, a set of calls like this would be made:

TableView.BeginUpdates();

// remove the second item
TableView.DeleteRows(new [] { NSIndexPath.FromRowSection(1, 0) });

// add item at the end
TableView.InsertRows(new [] { NSIndexPath.FromRowSection(5, 0) });

TableView.EndUpdates ();  

This is a simple example, but changesets can be quite complicated to describe. Your code 'describes the diff' between the current state and new state; a task that can be generalised but is not very fun. Getting it wrong results in the dreaded update assertion and crash:

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:]
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 1.  The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).' 

A diff algorithm is a diff algorithm, and should 'always work' once you get it right. Still, in an async-friendly world, inconsistency can also occur if the backing store is touched during an update cycle.

A common 'resolution' for issues of this nature was to abandon Begin/EndUpdates and call ReloadData, which causes a full/expensive, non-animated, reload of the entire table view. Apparently Apple was unhappy that this kept taking place, and now we have a new option.

New shiny - the diffable data source

With the introduction of iOS13, three new interesting classes appear in UIKit/Foundation. First, we have two new data source base classes:

  • UITableViewDiffableDataSource<TSectionIdentifier, TItemIdentifier>
  • UICollectionViewDiffableDataSource<TSectionIdentifier, TItemIdentifier>

Additionally, there's a 'snapshot' class:

  • NSDiffableDataSourceSnapshot<TSectionIdentifier, TItemIdentifier>

All three classes are generic to a section and item identifier type. The idea behind these classes is that rather than implement callbacks like NumberOfSections, and RowsInSection, we instead create and provide a snapshot of our data to a diffable data source using a new ApplySnapshot(snapshot, animatingDifferences) method, and iOS automatically determines the changes, updating the tableview contents and animating the transition for us.

Look ma! No manual diffing!

Cool right? By using a diffable data source, we save ourself the hassle of manually specifying the way changes should be animated. But how does a diffable data source know what has and hasn't changed?

You might reasonably guess that the diffing is performed by keeping track of which object instances are added or removed between snapshots. In practice, that would be too brittle - actions like refreshing data from an API typically result in the creation of an entire new set of objects - so the diffing needs to work more on a basis of semantic equivalence.

The mechanism for this ends up coming down to those two generic type arguments we saw earlier: TSectionIdentifier and TRowIdentifier. In native iOS land, these type arguments must conform to the Hashable protocol, which essentially defines the iOS equivalents of .NET's GetHashCode and Equals methods. Similar to .NET, in iOS the ultimate base class NSObject conforms to Hashable and the appropriate methods are exposed in Xamarin.iOS as GetNativeHash and IsEqual. UIKit uses these methods on the section and row types to decide whether items are being added, removed or rearranged between snapshots. So all we have to do is implement those, right?

Diffable powers without deriving from NSObject

The problem with having to implement GetNativeHash and IsEqual in Xamarin.iOS is that it requires types to be derived from NSObject, which (at least for me), is an unacceptable constraint. First and foremost, NSObject is not portable. Beyond that, deriving models from it precludes deriving them from other base types, and a need to derive from NSObject means you can't use types that you don't own, because you can't force them to derive from NSObject. What's the answer then? There are probably a few options, but the one I find least intrusive is the use of a wrapper type. The most basic implementation is one that forwards NSObject hash calls to the inner .NET type, like this:

Now, given a T that has a meaningful GetHashcode() implementation, we can pass an IdentifierType<T> to our diffable data source and snapshot methods. Another approach I like is to provide a Func<T,U> that will return the hash-friendly representation of T. That would look like this:

With the latter approach, we can use any type with diffable data sources, even if we don't own it and can't modify it to provide a meaningful GetHashCode() implementation.

Diffable powers without doubling your LOC

This still isn't that great. Wrapping the types means writing verbose, heavy handed code with a lot of type parameters all over the place. That said, it should be possible to enscapsulate that in a way that gives us a more pleasant consuming experience. I present my attempt, EasyUITableViewDiffableDataSource (name subject to change).

(It's easy to consume, not easy to read ok)

EasyUITableViewDiffableDataSource (name subject to change) exposes a much friendlier API to a C# consumer. It removes the need to think about anything NSObject-related, and lets the consumer work directly with .NET types, taking Funcs at the time of creation that configure section and row identifiers. It also exposes a new overload of the ApplySnapshot method that works directly with .NET types and handles the busywork of preparing a UIKit-friendly snapshot using our IdentifierTypes automatically. This is almost perfect, but it has one glaring issue - the type signature:

A type signature like the one above this poses two major issues. The first is verbosity and awkward usage - when you new an instance of a type like this, you have to specify each type parameter explicitly - it can't be inferred from constructor arguments.

The second is the brittleness - specifying each of the type parameters explicitly (in the constructor, and/or in a field/property definition) means lots of places to make changes if you decide your row or section identifier needs to be different. Not to worry, these concerns with EasyUITableViewDiffableDataSource (name subject to change) can be addressed.

<Diffable, powers<without<too, many>, Ts>>

The answer is a static helper that performs the creation of the data source. A static helper method is eligible for method argument type inference, which resolves the verbosity during use issue.

The brittleness can also be addressed by recognising that the existing set of generic type arguments expose what are primarily configuration/implementation details. The only interesting type argument to the consumer after the data source is created is the top level element type (TSection), which is what gets passed to the .NET friendly ApplySnapshot method. Knowing that, we can hide the gory details of the EasyUITableViewDiffableDataSource (name subject to change) behind an interface, and return the interface from our static helper method.

The helper also gives us the opportunity to address suboptimal API in the original diffable data source types, which require generic type arguments to be specified but do not make use of them when providing arguments for the getCell callback.

Was it worth it?

Whew, that was a ride. But now, a .NET friendly diffable data source can be configured very naturally:

As a reminder, that configuration gets you free tracking on every call to ApplySnapshot (same video as earlier):

The highlights from this approach:

  • No type arguments specified during creation - everything is inferred

  • The returned type, IUITableViewDiffableDataSource<Game> couples the code to just the core model type

  • No need to derive from NSObject, or to modify existing types, or to prepare snapshots directly

  • Automatic typed model in GetCell

  • Automatic change tracking!

The potential downsides from this approach:

  • Introduction of an unconstrained generic NSObject subclass. I am not well informed enough to decide whether that's a major issue. In an interpreter world the idea that we know all possible Ts for a generic NSObject subclass at compile time is no longer valid, so maybe it's less of a problem these days

  • Additional allocations incurred during the creation of wrapper objects. Since iOS is a beast, it probably isn't a major concern. However, a modification to EasyUITableViewDiffableDataSource (name subject to change) could involve reuse of wrapper instances between snapshots.

  • Supported on iOS13 and above only.. enough said

  • (Probably one I should have mentioned earlier 💀) - bugs in the diffing. There are a few odd behaviours I've noticed as I stress test this with more complicated changesets, including the ability to crash the app. Given we're seeing fairly frequent updates to iOS at the moment, it might be worth waiting a little longer before going all in on this. . As it turns out, the bugs I encountered were actually in UITableView proper and not in the diffable data sources. Invoking Begin/EndUpdates directly against the UITableView with the same set of updates (as diffable does internally) causes the crash. I found this when Steve Breen (the guy in the WWDC video! :O) drove by my tweet about the bug, asked for a repro, and then verified the issue was upstream. As someone who once found themselves in a position where they were often being blamed for bugs in upstream dependencies, I am happy to see it wasn't a bug in diffable after all.

If you fall into the small niche of use cases in Xamarin.iOS that benefit from diffable data, I think that the upsides outweigh the downsides. If nothing else, the new data source types will make life easier in my demo apps and proof of concepts. If you're interested in giving it a try, the relevant classes are all available here.

Happy (not having to do) diffing!