UITableView Diffable DataSource Part 1
At WWDC 2019 Apple announced a couple of really cool features for table views and collection views. One of these cool features comes in the form of UITableViewDiffableDataSource
and its counterpart UICollectionViewDiffableDataSource
. These new diffable data source classes allow us to define data sources for collection- and table views in terms of snapshots that represent the current state of the underlying models. The diffable data source will then compare the new snapshot to the old snapshot and it will automatically apply any insertions, deletions, and reordering of its contents. I will suggest you read https://medium.com/@ali-akhtar/uicollection-compositional-layout-part-4-datasource-18cfe9f46b15 this blog until Getting Started section where I explained in detail about diffable datasource. This blog will try to cover use cases and special cases for using diffable datasource in actual project
What was the problem?
UITableView has a property called dataSource. It’s a type that conforms to UITableViewDataSource protocol. It’s responsibility is provide to tableView some infos like the number of sections, number of rows per section and which cell will be used by tableView on a specific indexPath. Doesn’t matter where the dataSource gets these infos, it’s responsible to provide them to tableView. The mandatory methods of UITableViewDataSource are:
When we update the tableView removing or updating a row, for example, we need to make sure that dataSource is in sync with tableView, removing this element as well. Otherwise, we’re out of sync, and it can leads to a crash.
The crash message is similar to the message bellow. In my case I had a section and three rows. I removed the first row without updating my array of elements, where my dataSource was getting the data.
Terminating app due to uncaught exception
‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).’
Understanding how a diffable data source is defined
A diffable data source is an object that replaces your table view’s current UITableViewDataSource
object. This means that it will supply your table view with the number of sections and items it needs to render, and it supplies your table view with the cells it needs to display. To do all this the diffable data source requires a snapshot of your model data. This snapshot contains the sections and items that are used to render your page. Apple refers to these sections and items as identifiers. The reason for this is that these identifiers must hashable, and the diffable data source uses the hash values for all identifiers to determine what's changed. Let's look at this a little bit more in-depth by exploring the type signatures of both the data source and the snapshot.
First, let’s explore the UITableViewDataSource
signature:
class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
The UITableViewDiffableDataSource
class has two generic types, one for the section identifier and one for the item. Both are constrained so that whatever type fills the generic type must conform to Hashable
struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>
where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
The snapshot object is a struct rather than a class, and it has the same generic parameters as the diffable data source it’s applied to. This means that you can’t apply a snapshot with a set of identifiers to a data source with different identifiers and your code will fail to compile.
Getting Started
As shown in Figure 1 we rendered UITableView by using old style UITableViewDataSource
and it is working fine, Now next step to replace it with UITableViewDiffableDataSource
As shown in Figure 2 and 3, we replaced UITableViewDataSource with UITableViewDiffableDataSource. The
data source is generic and defines a type for section identifiers and a type for the data that is listed. The cell provider will take this generic as output for the third argument containing the data for which a cell needs to be returned.Data is provided through so-called snapshots: a snapshot of data. This already describes how diffable data sources work. Snapshots of data are compared with each other to determine the minimum amount of changes needed to go from one snapshot to another. The data source is smart enough to calculate the differences and replaces code like performBatchUpdates, for example.Diffable Data Sources result in code that is more compact and centralized while at the same time bringing several benefits. In the end, the system takes over a lot of the heavy lifting. To make you understand what this all includes it’s good to list some of the benefits.Few things we did in below code
- Init
NsDiffableDataSource
. It's a generic type, like theUITableViewDiffableDataSource
. - Add the
main
section and the data in it. - Apply the
snapshot
ondataSource
. - animatedDifferences as false, this parameter as false means that we don’t want the tableView getting animated in this snapshot.
In fact, we don’t want to animate at the first iteration. We want the first elements already in the tableView, not seeing them getting added on it.
This is our application where we are showing list of payment breakdown typically for ride hailing application
As shown in Figure 5 , after I removed Hashable
conformance to PaymentDetailsModel
I am getting error,
In Figure Figure 6 we added paymentDetail5
with same value as paymentDetail4
and application got crash Fatal: supplied item identifiers are not unique.
because current logic to create hash Value using data in each property , and diffable data source must have unique hash value in snapshot , if snapshot have same hasvalue object it will crash so , always be sure about that
As you can see in Figure 7
- For 1 section example we only conform to Hashable which means all value in the object should be same to generate same hash value and same for
==
operator - For 2 section example we conform to Hashable and say has should be generate with
serialNumber
which means if onlyserialNumber
value in object should be same to generate same hash value and for==
operator still whole object value will same - For last section if serialNumber in objects is same it will produce same hash and both object are equal as well
As shown in Figure 9 , after we add id
and make it unique , now the issue disappear, In the figure we say element in the model have unique hash
if their id
is unique also two PaymentDetailsModel
is equal
if both have same id
In figure 9 , we change the definition of Equtable , now paymentDetail4 and paymentDetail5 have unique hash but they are same / equal (beaucse title is equal). which is also not recommended and it leads to crash “Fatal: supplied item identifiers are not unique. Duplicate identifiers: {(\n TableViewDiffableDataSources.PaymentDetailsModel(id: \”5\”, title: \”Total\”, price: \”40.00\”, currency: \”$\”)\n)}” 0x00006000018c04e0". In short SectionIdentifier
and ItemIdentifier
in same snapshot should have hash and ==
unique, The question now arises how diffable data source uses hashValue and ==
in it’s algo we will see this later.
Rule of Thumb 1 : Snapshot data should have object that are unique hashValue
and all object with earch other follow!= to be true
Now we need to create some complex UI through diffable datasource. As you can see here three sections, one is showing Payment Breakdown , second is showing list of payment option you can select , and final current payment method that is selected. I would not go in much detail how I used Autolayout and build these cells and UI stuff. One thing I will like you to focus is the model we create to populate data. It conform to Hashable
As you can see in Figure 11 we created Section and Row enum , Row tell how many type of cell we have with Hashable
model as it’s associated type which should unique and when creating UITableViewDiffableDataSource, using item enum we are providing respective UI’s
UITableViewDiffableDataSource
The object you use to manage data and provide cells for a table view.
Declaration
@MainActor class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable
Discussion
A diffable data source object is a specialized type of data source that works together with your table view object. It provides the behavior you need to manage updates to your table view’s data and UI in a simple, efficient way. It also conforms to the UITableViewDataSource protocol and provides implementations for all of the protocol’s methods.
To fill a table view with data:
- Connect a diffable data source to your table view.
- Implement a cell provider to configure your table view’s cells.
- Generate the current state of the data.
- Display the data in the UI.
To connect a diffable data source to a table view, you create the diffable data source using its init(tableView:cellProvider:) initializer, passing in the table view you want to associate with that data source. You also pass in a cell provider, where you configure each of your cells to determine how to display your data in the UI.
We create snapshot and apply initial snapshot without animation. Note appendSections you should add sections first then add items to these sections. At this point everything working fine , we rendered UI using diffable datasource having complex multiple sections UI. In the next section we will do some experiment to understand diffable datasource more and will try to cover most of the scenarios you will face while building real application
Insert Section with animation
As shown in Gif 1 Initially snapshot having two section payment breakdown and payment selected. when user tap on Button Payment options section is inserted with left animation as easy . Previously you usually call performBatchUpdates that leads to sometimes crash when data is not sync with UI data. Now this is maintain by iOS itself which is really cool
As shown in Figure 13 let’s say when we land into screen we hit API that return data of two sections, we rendered two sections without animation, Now after we rendered we hit another endpoint that gives data os another section. what we do we insert that section later with animation so user can feel some data just came . as you can see we first get the data of old snapshot and then insert section and append items then everything work as in demo
If you not sure , which section is inserting or UI is totally driven from backend , you can create fresh snapshot as well and send it to the apply method it will automatically find diff what changed and do it’s magic, that’s why it is called diffable data source 💃
defaultRowAnimation →
The default type of animation to use when inserting or deleting rows.The default value of this property is UITableView.RowAnimation.automatic. If you set the value of this property, the new value becomes the default row animation for the next update that uses apply(_:animatingDifferences:completion:).
As shown in Gif2 we first insert coupon section with left animation and apply snapshot and on its completion we insert paymentType section with right animation
As shown in Gif3 we parallel insert coupon section with left animation and apply snapshot and we insert paymentType section with right animation parallel, Just see you don’t care about your datasource update with Ui update evrything is done by iOS , previously rembered performBatchUpdate if you don;t do this thing carefully you faced crashes Inconsistency crash
As shown in Figure 15 , we remove first item from payment details and add new item which is paymentDetail6 and it is working fine as shown in Figure 16
As shown in Figure 17 , when user tap on button we are updating paymentDetail4
and paymentDetail5
title value , but as you can see after I clicked on button value not updated what’s wrong, It’s because when we apply new snapshot it identify this item exists in both snapshot old and new using it’s id which is same , when it try to find any difference or value change in these objects it uses == operator but this also check id so for diffable data source no value change
Rule of Thumb 2: Diffable data source uses item hash to identify item. For the same item that exists in both old and new snapshots, diffable data source checks if the item changes by doing an “==” operation with its old and new values.
OR
Use HashValue that identify item instantly and == for the property that is using rendering UI, + both hash and == should unique in one snapshot
As shown in Figure 18 now we change the definition of Equatable now it identify paymentDetail4
in both old and new snapshot using it’s hash which actually it’s id and to check weather there is a change in paymentDetail4
object itself it uses Equtable
which is very deep and checking everything that we are rendering in UI to reflect changes. 🐼.
Note: In this blog we are focusing ItemIdentifier
value type not reference type like struct
. May be I will create another part where we will do experiment with reference type thing as well. Further please add comment if you are not agree with me or I misunderstand something as well so we can learn together
Diffable DataSource Summary
A TableView presents data in the form of sections and items, and an app that displays data in a TableView inserts those sections and items into the view. To support these actions, like inserting, deleting, moving, and updating data within a table view.
When populating a table view in an app, you can create a custom data source that adopts the UITableViewDataSource
protocol. To keep the information in the table view current, you determine what data changed and perform a batch update based on those changes, a process that requires careful coordination of inserts, deletes, and moves.
To avoid the complexity of that process, the sample app uses a UITableViewDiffableDataSource
object. A diffable data source stores a list of section and item identifiers, which represents the identity of each section and item contained in a table view. These identifiers are stable, meaning they don’t change. In contrast, a custom data source that conforms to UITableViewDataSource
uses indices and index paths, which aren’t stable. They represent the location of sections and items, which can change as the data source adds, removes, and rearranges the contents of a collection view. However, with identifiers a diffable data source can refer to a section or item without knowledge of its location within a table view.
As shown in Figure 19 , I removed Equatable
at all and used Swift’s default Equatable
implementation will check all properties for equality, to avoid extra code
If I remove hash(into hasher: inout Hasher) it will account all properties to create hash, we can do this but for now let’s use our custom or only id to make hashable or generate hashValue
We are calculating hash using id
and ==
we are checking whole object , if you look at the end result you will observe it is not actually update it delete first cell and insert cell at the same time. Also https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources by looking into the documentation
To use a value as an identifier, its data type must conform to the Hashable
protocol. Hashing allows data collections such as Set
, Dictionary
, and snapshots — instances of NSDiffableDataSourceSnapshot
and NSDiffableDataSourceSectionSnapshot
— to use values as keys, providing quick and efficient lookups. Hashable types also conform to the Equatable
protocol, so your identifiers must properly implement equality. For more information, see Equatable.
Because identifiers are hashable and equatable, a diffable data source can determine the differences between its current snapshot and another snapshot. Then it can insert, delete, and move sections and items within a collection view for you based on those differences, eliminating the need for custom code that performs batch updates.
Two identifiers that are equal must always have the same hash value. However, the converse isn’t true; two values with the same hash value aren’t required to be equal. This situation is called a hash collision. To increase efficiency, try to ensure that unequal identifiers have different hash values. The occasional hash collision is okay when it’s unavoidable, but keep the number of collisions to a minimum. Otherwise, the performance of lookups in the data collection may suffer.
What you can conclude from this you need to provide same implementation of Hashable
and Equtable
which is little confuse to me. In the next section we will use direct api of diffable datasource and can learn few things about diffable datasource
Directly Calling Diffable DataSource API’s
Insert Item
Delete Item
As shown in Figure 21 , we used Deletes the items with the specified identifiers from the snapshot
mutating func deleteItems(_ identifiers: [ItemIdentifierType]) →
Deletes the items with the specified identifiers from the snapshot.The array of identifiers corresponding to the items to delete from the snapshot.
You can delete entire sections
Update Item
We want to achieve this thing where initially 10 rows badge is showing and when we tap on cell it collapse to 5 row and then this thing continues