Skip to content

Commit

Permalink
Remember offset of collection views during table view reload to avoid…
Browse files Browse the repository at this point in the history
… jitter
  • Loading branch information
Anderas committed Apr 1, 2022
1 parent 867af20 commit c90456c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 14 deletions.
2 changes: 2 additions & 0 deletions Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 12 additions & 12 deletions Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -1486,7 +1486,7 @@ - (void)recentsListServiceDidChangeData:(id<RecentsListServiceProtocol>)service
forSection:(RecentsListServiceSection)section
totalCountsChanged:(BOOL)totalCountsChanged
{
NSInteger sectionIndex = -1;
NSInteger sectionIndex = DATA_SOURCE_INVALID_SECTION;
switch (section)
{
case RecentsListServiceSectionInvited:
Expand Down
107 changes: 107 additions & 0 deletions Riot/Modules/Common/Recents/RecentsContentOffsetStore.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
21 changes: 19 additions & 2 deletions Riot/Modules/Common/Recents/RecentsViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ @interface RecentsViewController () <CreateRoomCoordinatorBridgePresenterDelegat

@property (nonatomic, strong) SpaceChildRoomDetailBridgePresenter *spaceChildPresenter;

@property (nonatomic, strong) RecentsContentOffsetStore *contentOffsetStore;

@end

@implementation RecentsViewController
Expand Down Expand Up @@ -138,6 +140,8 @@ - (void)finalizeInit

displayedSectionHeaders = [NSMutableArray array];

_contentOffsetStore = [[RecentsContentOffsetStore alloc] init];

// Set itself as delegate by default.
self.delegate = self;
}
Expand Down Expand Up @@ -213,7 +217,7 @@ - (void)userInterfaceThemeDidChange
[ThemeService.shared.theme applyStyleOnSearchBar:self.recentsSearchBar];

// Force table refresh
[self.recentsTableView reloadData];
[self refreshRecentsTable];

[self.emptyView updateWithTheme:ThemeService.shared.theme];

Expand Down Expand Up @@ -394,7 +398,7 @@ - (void)refreshRecentsTable
// Force reset existing sticky headers if any
[self resetStickyHeaders];

[self.recentsTableView reloadData];
[self reloadTableViewWithContentOffsets];

// Check conditions to display the fake search bar into the table header
if (_enableSearchBar && self.recentsSearchBar.isHidden && self.recentsTableView.tableHeaderView == nil)
Expand All @@ -420,6 +424,19 @@ - (void)refreshRecentsTable
}
}

- (void)reloadTableViewWithContentOffsets
{
// We must store the current content offsets if each cell is a collection view
// and restore after the reload is done.
if (self.recentsDataSource) {
[self.contentOffsetStore storeContentOffsetsFor:self.recentsTableView dataSource:self.recentsDataSource];
[self.recentsTableView reloadData];
[self.contentOffsetStore restoreContentOffsetsFor:self.recentsTableView dataSource:self.recentsDataSource];
} else {
[self.recentsTableView reloadData];
}
}

- (void)hideSearchBar:(BOOL)hidden
{
[super hideSearchBar:hidden];
Expand Down
1 change: 1 addition & 0 deletions Riot/Modules/Home/Views/TableViewCellWithCollectionView.m
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ - (void)prepareForReuse
self.editionViewBottomConstraint.constant = 0;
self.editionView.hidden = YES;

[self.collectionView setContentOffset:CGPointZero];
self.collectionView.scrollEnabled = YES;
}

Expand Down
1 change: 1 addition & 0 deletions Riot/SupportingFiles/Riot-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@
#import "MXRoom+Sync.h"
#import "UIAlertController+MatrixKit.h"
#import "MXKMessageTextView.h"
#import "TableViewCellWithCollectionView.h"
1 change: 1 addition & 0 deletions changelog.d/5958.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rooms: Remember offset of collection views during table view reload to avoid jitter

0 comments on commit c90456c

Please sign in to comment.