From c90456c86e81a2fd9b8ea8e91d619d3b32e663ec Mon Sep 17 00:00:00 2001 From: Andy Uhnak Date: Fri, 1 Apr 2022 13:02:20 +0100 Subject: [PATCH] Remember offset of collection views during table view reload to avoid jitter --- .../Recents/DataSources/RecentsDataSource.h | 2 + .../Recents/DataSources/RecentsDataSource.m | 24 ++-- .../Recents/RecentsContentOffsetStore.swift | 107 ++++++++++++++++++ .../Common/Recents/RecentsViewController.m | 21 +++- .../Views/TableViewCellWithCollectionView.m | 1 + Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + changelog.d/5958.bugfix | 1 + 7 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 Riot/Modules/Common/Recents/RecentsContentOffsetStore.swift create mode 100644 changelog.d/5958.bugfix diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h index cf6329d0f7..597e93b07f 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h @@ -23,6 +23,8 @@ @class DiscussionsCount; @class MXSpace; +#define DATA_SOURCE_INVALID_SECTION -1 + /** List the different modes used to prepare the recents data source. Each mode corresponds to an application tab: Home, Favourites, People and Rooms. diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index ec5a4916f6..61a03de676 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -102,16 +102,16 @@ - (void)dealloc - (void)resetSectionIndexes { - crossSigningBannerSection = -1; - secureBackupBannerSection = -1; - directorySection = -1; - invitesSection = -1; - favoritesSection = -1; - peopleSection = -1; - conversationSection = -1; - lowPrioritySection = -1; - serverNoticeSection = -1; - suggestedRoomsSection = -1; + crossSigningBannerSection = DATA_SOURCE_INVALID_SECTION; + secureBackupBannerSection = DATA_SOURCE_INVALID_SECTION; + directorySection = DATA_SOURCE_INVALID_SECTION; + invitesSection = DATA_SOURCE_INVALID_SECTION; + favoritesSection = DATA_SOURCE_INVALID_SECTION; + peopleSection = DATA_SOURCE_INVALID_SECTION; + conversationSection = DATA_SOURCE_INVALID_SECTION; + lowPrioritySection = DATA_SOURCE_INVALID_SECTION; + serverNoticeSection = DATA_SOURCE_INVALID_SECTION; + suggestedRoomsSection = DATA_SOURCE_INVALID_SECTION; } #pragma mark - Properties @@ -402,7 +402,7 @@ - (void)dataSource:(MXKDataSource*)dataSource didStateChange:(MXKDataSourceState { if (dataSource == _publicRoomsDirectoryDataSource) { - if (-1 != directorySection && !self.droppingCellIndexPath) + if (DATA_SOURCE_INVALID_SECTION != directorySection && !self.droppingCellIndexPath) { // TODO: We should only update the directory section [self.delegate dataSource:self didCellChange:nil]; @@ -1486,7 +1486,7 @@ - (void)recentsListServiceDidChangeData:(id)service forSection:(RecentsListServiceSection)section totalCountsChanged:(BOOL)totalCountsChanged { - NSInteger sectionIndex = -1; + NSInteger sectionIndex = DATA_SOURCE_INVALID_SECTION; switch (section) { case RecentsListServiceSectionInvited: diff --git a/Riot/Modules/Common/Recents/RecentsContentOffsetStore.swift b/Riot/Modules/Common/Recents/RecentsContentOffsetStore.swift new file mode 100644 index 0000000000..9bab1eb3b1 --- /dev/null +++ b/Riot/Modules/Common/Recents/RecentsContentOffsetStore.swift @@ -0,0 +1,107 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +/** + Types of sections as defined in `RecentsDataSource`, albeit represented as an enum for stronger type safety + */ +enum RecentsDataSourceSectionType { + case crossSigningBanner + case secureBackupBanner + case directory + case invites + case favorites + case people + case conversation + case lowPriority + case serverNotice + case suggestedRooms +} + +/** + A store of content offsets for a special type of table view where each cell contains a collection view, + and the content offset needs to be preserved across table view reloads. + + This type of table view is used on some of the tabs (e.g. home), where vertical rows represent sections + (favourites, people, rooms ... ) and horizontal represents different rooms (shown in a collection view). + When such a table view is reloaded, it will dequeue and reuse the previously created collection views + in random order, meaning that any contentOffset they may have had will now be arbitrarily swapped around. + The store allows saving the current content offsets per section and restoring them after a reload. + */ +@objc class RecentsContentOffsetStore: NSObject { + private var contentOffsets = [RecentsDataSourceSectionType: CGPoint]() + + @objc func storeContentOffsets(for tableView: UITableView, dataSource: RecentsDataSource) { + reset() + let sections = sectionWithTypes(for: tableView, dataSource: dataSource) + + for (section, sectionType) in sections { + + let indexPath = IndexPath(row: 0, section: section) + guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCellWithCollectionView else { + continue + } + + contentOffsets[sectionType] = cell.collectionView.contentOffset + } + } + + @objc func restoreContentOffsets(for tableView: UITableView, dataSource: RecentsDataSource) { + let sections = sectionWithTypes(for: tableView, dataSource: dataSource) + + for (section, sectionType) in sections { + + let indexPath = IndexPath(row: 0, section: section) + guard + let offset = contentOffsets[sectionType], + let cell = tableView.cellForRow(at: indexPath) as? TableViewCellWithCollectionView + else { + continue + } + + cell.collectionView.contentOffset = offset + } + reset() + } + + private func reset() { + contentOffsets = [:] + } + + private func sectionWithTypes(for tableView: UITableView, dataSource: RecentsDataSource) -> [(Int, RecentsDataSourceSectionType)] { + // We associate the arbitrary integer index of a section at any particular time with its semantic value (e.g. 2 = `favorites`). + // At this point the index could equal -1 because not all indexes in data source ara valid for every screen + return [ + (dataSource.crossSigningBannerSection, .crossSigningBanner), + (dataSource.secureBackupBannerSection, .secureBackupBanner), + (dataSource.directorySection, .directory), + (dataSource.invitesSection, .invites), + (dataSource.favoritesSection, .favorites), + (dataSource.peopleSection, .people), + (dataSource.conversationSection, .conversation), + (dataSource.lowPrioritySection, .lowPriority), + (dataSource.serverNoticeSection, .serverNotice), + (dataSource.suggestedRoomsSection, .suggestedRooms) + ].filter { (section, sectionType) in + + // We only want indexes that are valid for a given view and actually shown on the table view + section != DATA_SOURCE_INVALID_SECTION + && section < tableView.numberOfSections + } + } +} diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index ea12f0516a..177b1a9529 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -88,6 +88,8 @@ @interface RecentsViewController ()