Implementing iOS Collection View with Efficient Rendering Mechanism

Tokopedia has a lot of pages with dynamic layout, meaning that the layout may change from time to time based on the data received from the backend side. There will be more pages like that to come in the future. Luckily, Apple has already provided a way to handle this in the form of UIKit’s UICollectionView.

UICollectionView is a powerful component that allows us to render multiple components associated with the data provided. It is also easy to implement its basic function to render a layout for the first time. However, the performance is not good while handling complicated layout.

So to give out the best user experience possible, we decided to use Texture’s ASCollectionNode instead which is almost the same as UICollectionView. ASCollectionNode has the better rendering performance because it is done in the background thread, making the main thread more available to respond for user interaction.

Cool, we got the component we need. However, implementing it is another story especially when we want to update our collectionView which can be a complicated process. No, I’m not talking about reloadData. Sure, it’s easy to use but its performance is not good at handling a huge number of cell updates because it works by recreating all the cells, even the ones that don’t need to be updated. That’s right, I’m talking about performBatch (Equivalent to UICollectionView’s performBatchUpdates).

Unfortunately, to make performBatch works properly, we must define which cells are to be handled (delete, insert, update, move) manually. Defining the wrong indexPaths might lead to crashes. Here’s an example of the crash:

Fatal Exception: NSInternalInconsistencyException
Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (3) must be equal to the number of sections contained in the collection view before the update (1), plus or minus the number of sections inserted or deleted (1 inserted, 0 deleted).

Wouldn’t it be wonderful if this process can be automated? So, we decided to try making life easier for Tokopedia iOS Devs by creating an improvement of ASCollectionNode with better API that is inspired by IGListKit and SwiftUI’s List in which we will call ListNode. Here is a preview of ListNode implementation:

Looks easier than the implementation of ASCollectionNode, right? When we want to implement a collectionView, we don’t need to worry about defining its dataSource and indexPaths for performBatch. Now, let’s talk about what happened behind it, shall we?

Below The Surface 👀

ListNode have its own built-in dataSource. There are 3 things that are defined in it, as seen in the code below:
1. Every section holds only one item.
2. The number of sections will be based on the number of data. The data is stored inside a property.
3. The cells that represent the data are generated based on the closure defined by the user.

After reading the code above, you might be wondering about two things:
1. What is HashDiffable?
2. Why would I need to store the data inside ListNode?

Well, those two are needed for improving our performBatch method. We need to assign an identity using HashDiffable to each of the data to allow ListNode to perform diffing. Diffing is a technique to find the difference between the old data that is stored inside ListNode and the new data when we call performUpdates method. Believe it or not, the result will be the indexPaths for inserts, deletes, updates and moves operations!

Don’t forget to update your model that represents the cells to conform to HashDiffable too!

So.. how does diffing work? We will give you three different cases of performUpdates and explain what diffing will do.

Diffing in a Nutshell 🥜

Case 1
We want to perform batch updates with:
Old data: [A, B, C, D, E]
New data: [B, C, D, E, F, G]

The new data doesn’t have A which is located at index 0 in the old data. So, we perform delete at index 0. Then, the new data has F and G at index 4 and 5 respectively. Thus, insertion must happen at index 4 and 5 too.

Diffing result:
Deletes: [0]
Inserts: [4, 5]

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Case 2
We want to perform batch updates with:
Old data: [A, B, C, D, E]
New data: [A, D, C, B, E]

This time the members of the data don’t change, but their positions do. We have B located at index 1 in the old data and index 3 in the new data. We also have D located at index 3 in the old data and index 1 in the new data. In conclusion, B is moved from index 1 to 3 and D is moved from index 3 to 1.

Diffing Result:
Moves: [(from: 1, to: 3), (from: 3, to: 1)]

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Case 3
We want to perform batch updates with:
Old data: [A, B, C, D, E]
New data: [A, B, F, D, E]

In this case, what is located at index 2 in the old data is C. Meanwhile, in the new data index 2 is F. So, we delete C and then insert F at index 2. This can be simplified as an update at index 2.

Diffing result:
Updates: [2]

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

For the source code of diffing, we recommend checking IGListDiff. It is the diffing algorithm of IGListKit. Then, maybe you can create your own diffing algorithm based on your needs!

To Sum It Up 📚

ASCollectionNode is powerful at rendering a huge number of cells. However..

When implementing its performBatch method, we must make sure we don’t give the wrong indexPaths to the insert, delete, update and move instructions. By creating ListNode, we can simply remove the biggest hurdle of using ASCollectionNode.

If your project still uses UIKit, don’t worry. You still can do the improvement on UICollectionView because the API is not that different. There are some additional things to handle though:

  • Cells registration
  • Reusable cells dequeueing
  • Cells size calculation

These three are handled automatically in ASCollectionNode, but not in UICollectionView. Other than that, you’re good to go!

Maybe some of you are wondering. At this point, why not use SwiftUI’s List instead? We would if we could.. but SwiftUI is only available on iOS 13 and above. Meanwhile, there are still a lot of Tokopedia user devices that are below iOS 13. We made ListNode’s API to be similar to List to prepare us when the time comes to migrate to List.

One last thing, if you want to dive deeper into our process in creating ListNode, make sure to check this one out!

Credits 🙇

I would like to mention that I don’t work on ListNode alone and I don’t think I can do that. Thank you Edho Prasetyo, Ambar Dwi Septian, and Digital Khrisna Aurum for working together with me on this one!

As always, we have an opening at Tokopedia.
We are an Indonesian technology company with a mission to democratize commerce through technology and help everyone achieve more.
Find your Dream Job with us in Tokopedia!
https://www.tokopedia.com/careers/

--

--

--

Story from people who build Tokopedia

Recommended from Medium

All about debounce: 4 ways to achieve debounce in Swift.

Optional ViewBuilder closures in SwiftUI

Swift 5.7’s Optional Unwrapping Syntax

iOS Interview Questions Part 9

Implementing Coordinator Pattern in iOS

Using Apple’s Testing Mjölnir: XCUITest

Conditionally apply modifiers in SwiftUI

How to conditionally apply a modifier to a view in SwiftUI.

iOS — Advanced Memory Debugging to the Masses​

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Kensen Tjoa

Kensen Tjoa

More from Medium

Distributing Binary Frameworks as Swift Packages

SwiftDependencyChecker — check CocoaPods, Carthage and Swift PM dependencies for known…

Test Driven Development: Simple Flow Object in iOS

Introducing the Agoda Widget: What is it, and why did we develop it?