Messenger Seen heads animation on iOS

Different ways of building seen heads animation like Facebook Messenger.

Zhisheng Huang
7 min readFeb 18, 2018

Introduction

Facebook Messenger has a special seen state animation when your message recipient(s) have seen your message. It’s meant to give sender an informational notice that your audiences are actively online and had seen your message in real time. The business value for this feature is not only to encourage people to send more real time messages, but also gives some delightful joy to people who messages a lot. This feature is available in 1:1 chat as well in group chat. I have created a short gif to illustrate the seen heads move animation:

So where do we start?

#1 Vanilla UICollectionView

To start off, one thing that I am sure, is that we should use UICollectionView.

Not only it supports customized layout, UICollectionView also has native animation for insert/update/delete/move operations. So the most important bit here is the move animation. From the Apple doc for UICollectionView:

func moveItem(at indexPath: IndexPath,             
to newIndexPath: IndexPath)

we can use it to achieve the move animation between seen heads. UICollectionView internally manages all the animation details and it handles all the heavy-lifting by configuring the right animation properties and send it over to Core Animation for the actual effect.

So now the question is, how do we construct the data model for this UICollectionView? It’s easy, we just need to create a one-dimensional array of objects, containing either a Message or a User(for seen head).

Now we can easily achieve this by:

  1. Create an array of ViewModel to mimic the data layer ordering;
  2. Create a customized UICollectionViewLayout which knows how to layout message cell and the seenHead cell;
  3. Hook up the data and call the move API on UICollectionView.

#2 IGListKit Embedded H-scroll

The Vanilla UICollectionView approach is good enough to fulfill our requirement to achieve move animation. However, things are getting hairy when the messages UI contains a lot of different type of cells. And as the business logic evolves, it would get more complicated for the logic inside the View Controller level. Most of the time, it would eventually become a massive-view-controller(the notorious MVC pattern).

Facebook Instagram had open sourced a new library called IGListKit back in 2016, which helps to resolve the complication using UICollectionView. After adopting it, I was impressed by how organized the architecture becomes, and the code becomes much easier to reason about. Moreover, it internally provides an efficient diff-ing algorithm which can be used to generate the minimum update operations based on the before and the after data model. Following the Nested example from here, we can make a HScrollSeenHeadSectionController to be an embedded section controller:

Now we have three section controllers, and the HScrollSeenHeadSectionController contains another H-scroll UICollectionView for the seen heads. This time I wasn’t planning to use a custom UICollectionViewLayout since I thought that the nested SectionController can handle automatically. However, this is what I get:

It turns out there are a few problems with using the nested section controller as the container for all the seen heads:

  • No move animation. The data model now becomes a 2-dimensional arrays and we have to mimic the updated data structure. And IGListKit don’t treat it as a move animation, because technically we cannot move a cell between two different UICollectionView(s).
  • Incorrect layout for the H-scroll UICollectionView. In order to layout the seen heads starting to align from the right edge, we have to implement a new customized UICollectionViewLayout specifically for this H-scroll component.

I gave up on this approach and I believe that needs to be addressed in the library level to have support for cell moves animation among different UICollectionView(s), which seems to be non-trivial.

#3 IGListKit + Custom UICollectionViewLayout

Lastly, I decided to use the Custom UICollectionViewLayout from #1 and marry it with IGListKit. Basically under the hood, the same idea, which is using the 1-dimensional array and use a single UICollectionView to power all the UI elements on screen.

The benefit of using IGListKit, is that it’s all data-driven, no need to know about the UICollectionView details about when to move or when to delete. The library automatically calculates it for us and we can do:

and, bang, seen heads are animating beautifully!

#4 Use SupplementaryView for the seenHeads

While making seenHeads as part of the List Model works well with the out-of-the-box move animation provided by UICollectionView, it does come with the cost of complex data management and the lack of scalability. Imagine that we add a profile image and also allow it to move, then the data model array would become more complicated and harder to maintain.

How can we build the same seenHead animation with better scalability support? Turns out that we can use the supplementaryView from UICollectionView!

From the Apple’s doc about UICollectionView, it states that every indexPath can have their own set of supplementary views, e.g. we can have a “profile_image” supplementary view for the profile image inside a cell; we can have “message_send_state” supplementary view for the send state UI inside a cell; and we can also have a “time separator” supplementary view which is layout-ed above the message to denote the time(2018/03/15) inside a cell etc. So with supplementary view, we can create element kind “seen_head” for our seen heads inside a cell too!

First off, I create an enum which will represent 10 seen heads:

The reason to use 10 as a limit is that we probably won’t need unlimited seenHead, so each indexPath would at most have 10 seenHeads. The good thing about this is that each indexPath can have their own 10 seenHeads. (For any extra seen heads which overflow, we could use a ‘+’ icon in UI to represent those.)

Next, since we use supplementary view for our seenHeads, the data model becomes much simpler. Previously we had to mix the Messages and SeenHead Users into the same array which is weird, now we can just construct a relational data model between Message and SeenHead, so a message can own an array of seenBy users:

IGListKit also provideds us a handy nice protocol “<ListSupplementaryViewSource>”, which can be used to provide supplementary views inside the “ListSectionController

The key to use UICollectionView’s supplementaryView to achieve the seen head animation is the following APIs:

The first two APIs can be used to define the initial appearing layout and the final disappearing layout for certain seenHead, and the last two APIs will be used to define which seenHeads need to do the insert/delete updates:

For example, from the gif below:

If we tap on the right most seenHead at Row 3, there are two seenheads need to be animated, so our set of inserts and deletes updates would be:

  1. Inserts on [section 3, item 0], element kind: seenhead1
  2. Deletes on [section 2, item 0], element kind: seenhead0,
  3. and since we are moving the position which would affects any seenheads after seenhead0, we will need to add insert for [section 2, item 0], element kind: seenhead1 as well.
  4. Lastly for the initialLayoutAttributesForAppearingSupplementaryElement and finalLayoutAttributesForDisappearingSupplementaryElement, we could associate userId with the previous layout attributes with the current layout attributes, which we can do a fast lookup and implement the two APIs easily.

The gist to use the supplementary view approach is to calculate correctly which supplementary view needs to be deleted and inserts, otherwise the data source API “viewForSupplementaryElementOfKind” won’t get called which would leave the seenHead unupdated.

Feel free to check out the source for the naive implementation of the supplementary view approach: Link

Learning

Surely there are a lot of ways to achieve this animation by building your own customized UIScrollView where can control all the granularity of animation or layout. But I found using UICollectionView with custom layout is the easiest approach.

There are many other interesting technical details for this little weekend project:

  • The implementation details of how we generate the custom UICollectionViewLayout for the seen head + messages for production quality code, as the challenge is how to build an efficient layout which has 60fps scroll perf.
  • The challenge of client/server communication for seen states, especially in a group chat environment where many asynchronous seen head mutation happens. Handling that at scale is definitely an interesting engineering problem.

Thanks for reading! Let me know if you have any question and welcome for feedback!

References

  1. https://github.com/Instagram/IGListKit/
  2. https://www.objc.io/issues/1-view-controllers/lighter-view-controllers/
  3. https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52
  4. https://developer.apple.com/documentation/uikit/uicollectionview

--

--