Messagekit: Add support for incremental loading of messages when scrolling upwards

Created on 29 Jun 2019  Â·  14Comments  Â·  Source: MessageKit/MessageKit

Most chat applications have support for incremental loading, that is, as you scroll up to see history, it is loaded on demand.

Any serious usage of MessageKit with big conversations means a up to a half a second delay in any complex use case when calling reloadData. reloadDataAndKeepOffset is even worse.

Is there support for incremental loading anywhere and I have missed it? If not this should be included.

enhancement

Most helpful comment

I'm facing the same problem here. It takes almost 2 seconds to load around 7k items. The problem is basically because the flow layout calls sizeForItem for all items, even if they're not visible.

I've made my own control to fetch message from CoreData and insert at index 0, but the problem remains because, after the new batch of messages, I need to call reloadData, which makes the FlowLayout to call sizeForItem, so after some "pagination", the problem will be there.

This behavior is default on UICollectionViewFlowLayout, so we're dependent on it. Maybe a refactoring could be considered, using the UICollectionViewLayout inheritance instead of UICollectionViewFlowLayout. Or maybe consider UITableView.

Btw, reloadDataAndKeepOffset doesn't work in that case, because it stops the scroll, this snippet solves the problem:

let oldOffset = self.messagesCollectionView.contentSize.height - self.messagesCollectionView.contentOffset.y
messagesCollectionView.reloadData()
messagesCollectionView.layoutIfNeeded()
messagesCollectionView.contentOffset = CGPoint(x: 0, y: messagesCollectionView.contentSize.height - oldOffset)

I'm considering to open a PR to replace the current reloadDataAndKeepOffset.

Anyways, I'm still investigating possibilities to improve the performance, maybe using the prefetching API, but for know the framework has some limitations.

All 14 comments

What do you mean reloadDataAndKeepOffset is even worse? Do you have a better solution?

Sent with GitHawk

reloadDataAndKeepOffset is worse by forcing a layout to calculate a scroll offset. It could probably do some of the calculation in the background, as a lot of the constants are know. I haven't investigated what approach other messaging apps take to this.

Either way, that won't help with the fundamental cause of MessageKit not being designed to handle huge conversations. Incremental loading is a must-have feature in a messaging framework that is scalable.

Right now I don't have the bandwidth, but I look forward to hearing thoughts onto how we can implement this in MessageKit itself.

I mean you could insert new sections for your new messages at the top and move the existing ones down with the collection view APIs, but beyond that we are limited to the performance of UICollectionView. It sounds like you want something super scalable for large message groups like Slack or Facebook, but that wasn't a design goal of the framework.

Sent with GitHawk

I think part of the performance problem of MessageKit is that it bundles kind and the actual storage driving the drawing of the string. Metadata with (expensive in certain cases) data.

So for example MessageCollectionViewFlowLayout's cellSizeCalculatorForItem does a switch on kind to know which calculator to use, and ignores the associated value.

Every time you scroll kind is called again to compute things that have already been computed.

The kind implementation of MessageType may create an expensive NSAttributedString. So this is a serious problem when the same string is created multiple times when it isn't needed.

Further complicating the issues is that kind is implemented on the message object itself and not as a delegate method, so the solution of implementing NSCache for attributed strings (which is recommended) will require reaching outside of the object to a global cache, as it makes no sense to have a cache per message object.

This is not a problem with methods like cellTopLabelAttributedText() and so on, since they are all cached by NSCache in my implementation.

What do you think of this? Do you think it makes sense to decouple kind and data? Do we need to add a delegate method to fetch data that can be cached? We need to figure out a way to cache it, and if we make it part of MessageKit we would need to find a way to enable cache invalidation.

Update: I was able to derive huge performance improvements by caching kind calls (by passing a reference to an NSCache to each message, ugly I know, but this is an API design constraint) and avoiding MessageKit's scrollToBottom in favor of properly timed UICollectionView.scrollToItem calls (3x faster).

Yes I will take whatever makes it faster please

I'm facing the same problem here. It takes almost 2 seconds to load around 7k items. The problem is basically because the flow layout calls sizeForItem for all items, even if they're not visible.

I've made my own control to fetch message from CoreData and insert at index 0, but the problem remains because, after the new batch of messages, I need to call reloadData, which makes the FlowLayout to call sizeForItem, so after some "pagination", the problem will be there.

This behavior is default on UICollectionViewFlowLayout, so we're dependent on it. Maybe a refactoring could be considered, using the UICollectionViewLayout inheritance instead of UICollectionViewFlowLayout. Or maybe consider UITableView.

Btw, reloadDataAndKeepOffset doesn't work in that case, because it stops the scroll, this snippet solves the problem:

let oldOffset = self.messagesCollectionView.contentSize.height - self.messagesCollectionView.contentOffset.y
messagesCollectionView.reloadData()
messagesCollectionView.layoutIfNeeded()
messagesCollectionView.contentOffset = CGPoint(x: 0, y: messagesCollectionView.contentSize.height - oldOffset)

I'm considering to open a PR to replace the current reloadDataAndKeepOffset.

Anyways, I'm still investigating possibilities to improve the performance, maybe using the prefetching API, but for know the framework has some limitations.

@marcetcheverry

What do you think of this? Do you think it makes sense to decouple kind and data? Do we need to add a delegate method to fetch data that can be cached? We need to figure out a way to cache it, and if we make it part of MessageKit we would need to find a way to enable cache invalidation.

Figuring out how to make layout speed faster while maintaining an API that limits undefined behavior was a huge challenge for me. If you find a way to opt into caching using a custom strategy I'd be happy to hear any ideas.

@bguidolim

I'm facing the same problem here. It takes almost 2 seconds to load around 7k items. The problem is basically because the flow layout calls sizeForItem for all items, even if they're not visible.

This is really just one of the burdens of UICollectionView. Since layout has to be done up front, its really recommended to keep your datasource small. If your chat history has 7k messages, I recommend purging earlier content as you load more.

I’ve implemented a logic of pagination and I override “sizeForItem” to cache the sizes after the first calculation for a specific item. It’s working good, even though I prefer to use the prefetch API, which is a little bit more complicated when we have a inverted scroll.

As MessageKit is a framework and doesn’t have the responsibility of managing data, the calculation takes a long time, so the values must be cached.

Overall, I’m happy with the results I got. I’m managing to spend some time and maybe make a proposal of prefetch API on MessageKit.

Hi @bguidolim , could you suggest for me the way to do the "scroll upward to load more" feature using this framework?

@hainhathoang88 you could save the messages to Core Data and use the fetchLimit property to load N amount of messages in the refresh control action. Alternatively, you could fetch more messages from an end point in the refresh control action.

@Alexander-Frost Hi, thanks for replying me. Could you tell me more about the fetchLimit property? In the sample app, I can only find the load more using UIRefreshControl, which requires user to pull to refresh.

@hainhathoang88 You're going to have to do your own research on Google about Core Data

I’ve implemented a logic of pagination and I override “sizeForItem” to cache the sizes after the first calculation for a specific item. It’s working good, even though I prefer to use the prefetch API, which is a little bit more complicated when we have a inverted scroll.

As MessageKit is a framework and doesn’t have the responsibility of managing data, the calculation takes a long time, so the values must be cached.

Overall, I’m happy with the results I got. I’m managing to spend some time and maybe make a proposal of prefetch API on MessageKit.

Any update on Prefetch API? Can you suggest a good pagination logic for core data?

Was this page helpful?
0 / 5 - 0 ratings