TableView scrolling to top after applying UITableViewDiffableDataSource snapshot

1.3k Views Asked by At

I'm doing pagination using UITableViewDataSourcePrefetching.

The values will be taken from the Realm local storage.

I will get an array of objects. These values will be applied to the existing UITableViewDiffableDataSource datasource.

After applying snapshot the tableview scrolling to the top.

I have verified that all my ChatMessage objects has unique hashValues.

How can I prevent the scrolling?

Link to the video TableView_scroll_issue_video

Given my code snippet

private func appendLocal(chats chatMessages: [ChatMessage]) {
    var sections: [String] = chatMessages.map({ $0.chatDateTime.toString() })
    sections.removeDuplicates()
    guard !sections.isEmpty else { return }
    var snapshot = dataSource.snapshot()
    let chatSections = snapshot.sectionIdentifiers
    sections.forEach { section in
        let messages = chatMessages.filter({ $0.chatDateTime.toString() == section })
        /// Checking the section is already exists in the dataSource
        if let index = chatSections.firstIndex(of: section) {
            let indexPath = IndexPath(row: 0, section: index)
            /// Checking dataSource already have some messages inside the same section
            /// If messages available then add the recieved messages to the top of existing messages
            /// else only section is available so append all the messages to the section
            if let item = dataSource.itemIdentifier(for: indexPath) {
                snapshot.insertItems(messages, beforeItem: item)
            } else {
                snapshot.appendItems(messages, toSection: section)
            }
        } else if let firstSection = chatSections.first {
            /// Newly receieved message's section not available in the dataSource
            /// Add the section before existing section
            /// Add the messages to the newly created section
            snapshot.insertSections([section], beforeSection: firstSection)
            snapshot.appendItems(messages, toSection: section)
        } else {
            /// There is no messages available append the new section and messages
            snapshot.appendSections([section])
            snapshot.appendItems(messages, toSection: section)
        }
    }
    dataSource.apply(snapshot, animatingDifferences: false)
}
5

There are 5 best solutions below

2
On

You can try creating a new snapshot instead of referencing & mutating the current snapshot of your dataSource.

4
On

You can try the following:

dataSource.applySnapshotUsingReloadData(snapshot, completion: nil)

It's only available from iOS 15.0

Also common mistake is using UUID().uuidString.
Make sure that the id of each cell is fixed. UITableViewDiffableDataSource will not be able to properly calculate changes and it will reload the whole table if ids are updated.

0
On

After looking a bit into it, I realized that you apply the snapshot without animation. That means that on ios versions 13-14, you are not performing a diff, but actually reloading the data, which means the whole state of the dataSource is lost. Hence the scrolling position is also reset. You need to apply the snapshot with animation on versions 13, 14. There are a few work arounds if you insist on not animating the changes while setting ‘animated’ as true, but I don’t see, from a UX perspective, why you’d want that.

0
On

I think that everything works correctly. You scroll to the "past" and eventually you want to add more "old" messages. So you insert new messages into the indexPath (0-0). Your contentOffset stays the same so you see the "old" messages.

Calculating and preserving the visually correct contentOffset is possible but hard, and it's also pretty fragile. Whatever goes wrong your scroll position will "jump". You don't want to touch your contentOffset.

What you might actually want is to reuse the commonly famous trick that all popular messengers use. You want your "newest" messages to actually be on the bottom of the screen, and in the same time you'd like the newest message to be the first one in your dataSource.

So you need to flip the tableView vertically and then flip every cell vertically as well. Doing so you'll get more "natural" data source when the newest messages get inserted into the indexPath (0-0), and "older" messages get appended to the end of your message array.

let t = CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: 0, ty: 0)
tableView.transform = t
cell.transform = t

That might sound crazy, but give it a try and you'll be impressed.

0
On

If anyone facing the issue, I have resolved the issue by adding the section and items one at time with animation instead of in batches. The batching appears to be causing the jumping issue more than if done individually in the loop. The code is given below,

func appendLocal(chats chatMessages: [ChatMessage]) {
    var sections: [String] = chatMessages.map({ $0.chatDateTime.toString() })
    sections.removeDuplicates()
    guard !sections.isEmpty else { return }
    var snapshot = dataSource.snapshot()
    let chatSections = snapshot.sectionIdentifiers
    for section in sections {
        let messages = chatMessages.filter({ $0.chatDateTime.toString() == section })
        if messages.isEmpty {
            continue
        }
        /// Checking the section is already exists in the dataSource
        if let index = chatSections.firstIndex(of: section) {
            let indexPath = IndexPath(row: 0, section: index)
            /// Checking dataSource already have some messages inside the same section
            /// If messages available then add the recieved messages to the top of existing messages
            /// else only section is available so append all the messages to the section
            if let item = dataSource.itemIdentifier(for: indexPath) {
                snapshot.insertItems(messages, beforeItem: item)
            } else {
                snapshot.appendItems(messages, toSection: section)
            }
        } else if let firstSection = chatSections.first {
            /// Newly receieved message's section not available in the dataSource
            /// Add the section before existing section
            /// Add the messages to the newly created section
            viewModel.chatSections.insert(section, at: 0)
            snapshot.insertSections([section], beforeSection: firstSection)
            snapshot.appendItems(messages, toSection: section)
        } else {
            /// There is no messages available append the new section and messages
            viewModel.chatSections.append(section)
            snapshot.appendSections([section])
            snapshot.appendItems(messages, toSection: section)
        }
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}