Ever since Pull Down to Refresh was introduced on iOS we’ve had it implemented as the main way to keep data fresh in our iOS app. With updates going out from your queue throughout the day, the update collection view within Buffer can change pretty often.
These update collection views are used for our Pending, Sent, Contributions and Content Inbox, which makes up quite a portion of the app. Since v6.0 we’ve been using AsyncDisplayKit to do all of our rendering for these collection views which has allowed us to keep at the magical 60 frames per second needed for smooth scrolling.
Since we feel really good about our app’s rendering performance, we wanted to take another look at how to keep data fresh without effort from the user. Of course, that made us think about some interesting implementation details – and we quickly realized we needed a new approach to facilitate these changes.
With v6.3 we’ve introduced Instagram’s IGListKit, which automatically diffs objects to create inserts, deletions and moves before performing batch updates on the collection view. This instantly does away with many issues that crop up with UICollectionViews and removes the need to manually write batch updates ourselves. This setup lends itself perfectly to our goal of refreshing data automatically.
Once IGListKit was slotted in, we paired it with a new Pusher implementation. Pusher provides easy to use SDKs that publish and receive events over websockets. Our web app has used Pusher for years, but this is the first time introducing them on mobile. By hooking up the iOS app to existing channels and events we’ve been able to setup v6.3 to automatically refresh whenever content is added, deleted or edited. This refreshed content is then pushed to IGListKit, which diffs the objects to figure out what to insert, update or delete before passing things over to AsyncDisplayKit to render.
For example, if a user deleted an update in the queue you’re viewing you’ll see it fade out without requiring a pull down to refresh. The same thing would happen if you added an update on our web app, you’d see it animate in on your iOS device.
Initially, when I looked at hooking up IGListKit I started hacking away at our current code to get it up and running. Quickly realizing that due to the way IGListKit works, rewriting our table view code was the best route. Most of the code had been around for a couple of years and AsyncDisplayKit was added into the mix last August, so things needed a bit of a tidy up. We were also looking for a good time to move over to UICollectionView, which IGListKit currently requires.
There’s some great content out there for IGListKit, including a couple of talks from the folks at Instagram themselves. These were super useful for getting our heads around how IGListKit works. While some of the examples are slightly out of date, Ryan Nystrom’s talk walks through why Instagram built IGListKit and how it differs to current collection view conventions.
There were a few things that took a minute to figure out. AsyncDisplayKit has out of the box conventions for ASCollectionNode & ASTableNode data sources that, when compared to UITableView and UICollectionView, are super easy to get your head around. Since we rewrote our app with AsyncDisplayKit, we were quite comfortable with its conventions at this point.
With IGListKit it’s a little trickier. Now, you say goodbye to UICollectionViewDatasource, and instead use an IGListAdapter with a IGListAdapterDataSource. The data source doesn’t return counts or cells, instead it provides an array of Section Controllers. Section Controllers are then used to configure & control cells within the given collection view section.
This took some time for us to fully understand, but fortunately we had a working proof of concept showing updates in a collection view, albeit minus the extra UI provided by supplementary views, running rather quickly.
The biggest challenge was adding the ability to reorder updates in the pending queue as both AsyncDisplayKit and IGListKit don’t support the native moves offered in UIKit. Previously Jordan built out an implementation using simply AsyncDisplayKit, but IGListKit made this more complicated as updates were now across multiple Section Controllers rather than a single data source.
On top of that, previously we had only needed to reorder cells vertically – but now we also needed to do so in any direction which added more complexity. Our original solution is available on GitHub, and we plan to open source our updated version that we made for AsyncDisplayKit & IGListKit reordering soon.
In order to achieve this, we’ve cheated a little and created a reordering collection view which is shown to the user when a long press gesture is performed on an update. Once shown, this hides any UI elements that aren’t required for reordering and allows you to move the update as you normally would by dragging up, down, left or right.
When an update is dropped we then perform the API call to switch the order of the updates and trigger a refresh of the pending queue to get the new order. Then, we simply hide the reorder collection view and sync up content offsets to reveal the newly updated queue with all of the UI back in place.
We’re hoping that updates to both IGListKit & AsyncDisplayKit will allow us to remove this workaround in the future, however we’re pretty happy with how it feels right now.
We’ve already added even more Pusher logic into the app, such as updating the way we handle video transcoding to automatically refresh updates once transcoding is completed. We’re excited to also explore improving our Instagram Reminders onboarding to progress the web onboarding automatically.
We’re also excited about the possibility of updating existing cells if the content associated with them has changed with a new API to allow cell nodes to prevent a full reload. This would be super useful within Buffer. For instance, if you adjust an update from the middle of your queue to the top, the only update we need to make visually would be the scheduled time label in the top left of the cell. Currently, that scenario requires a full reload to adjust, which is quite expensive.
It’s not only update views that could benefit from AsyncDisplayKit & IGListKit, we’re thinking of updating a couple of our other screens like the profile selector and composer to make use of them in some capacity. We’ve found that once you get used to setting up tables or collection views without having to use reuse identifiers and other niceties these APIs provide, it’s a little hard to go back.
Eventually, an ambitious goal we’ve set is to remove pull to refresh altogether. We need to do some user research around how that feels especially as it’s become the expected way to ensure everything is in sync.
Huge thanks to the super smart folks at Pinterest who work on AsyncDisplayKit! They’ve helped out with any issues we’ve come across and have heard our wild feature requests. We’ve been able to take Buffer to some exciting new places because of it. We’re super grateful for the IGListKit and Pusher teams too, who helped make all of this possible.
We’d love to share more, if theres anything specifically you’re curious about reach out and we’d be happy to share. We’re also keen to share some more technical deep dives on this implementation soon.