From 10ee054d0ca0be452b849230b811214d4c063fdc Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 19 Mar 2018 19:34:57 -0400 Subject: [PATCH] Avoid flicker when loading more on top Adjusting content offset in the CollectionViewLayout.prepareLayout method avoids a flicker vs. the previous way we were doing it. // FREEBIE --- .../MediaTileViewController.swift | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/Signal/src/ViewControllers/MediaTileViewController.swift b/Signal/src/ViewControllers/MediaTileViewController.swift index 0bc220592..27e75685a 100644 --- a/Signal/src/ViewControllers/MediaTileViewController.swift +++ b/Signal/src/ViewControllers/MediaTileViewController.swift @@ -36,6 +36,8 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe Logger.debug("\(logTag) deinit") } + fileprivate let mediaTileViewLayout: MediaTileViewLayout + init(mediaGalleryDataSource: MediaGalleryDataSource, uiDatabaseConnection: YapDatabaseConnection) { self.mediaGalleryDataSource = mediaGalleryDataSource @@ -51,12 +53,13 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe let availableWidth = screenWidth - CGFloat(kItemsPerRow + 1) * kInterItemSpacing let kItemWidth = floor(availableWidth / CGFloat(kItemsPerRow)) - let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() + let layout: MediaTileViewLayout = MediaTileViewLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) layout.itemSize = CGSize(width: kItemWidth, height: kItemWidth) layout.minimumInteritemSpacing = kInterItemSpacing layout.minimumLineSpacing = kInterItemSpacing layout.sectionHeadersPinToVisibleBounds = true + self.mediaTileViewLayout = layout super.init(collectionViewLayout: layout) } @@ -279,7 +282,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe } // MARK: MediaGalleryDelegate - public func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) { + fileprivate func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) { Logger.debug("\(logTag) in \(#function)") self.delegate?.mediaTileViewController(self, didTapView: cell.imageView, mediaGalleryItem: item) } @@ -287,8 +290,8 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe // MARK: Lazy Loading // This should be substantially larger than one screen size so we don't have to call it - // multiple times in a rapid succession, but not so large that loading more get's chopping - let kMediaTileViewLoadBatchSize: UInt = 80 + // multiple times in a rapid succession, but not so large that loading get's really chopping + let kMediaTileViewLoadBatchSize: UInt = 40 var oldestLoadedItem: MediaGalleryItem? { guard let oldestDate = galleryDates.first else { return nil @@ -350,10 +353,15 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe } isFetchingMoreData = true - let scrollDistanceToBottom = oldContentHeight - contentOffsetY - CATransaction.begin() CATransaction.setDisableActions(true) + + // mediaTileViewLayout will adjust content offset to compensate for the change in content height so that + // the same content is visible after the update. I considered doing something like setContentOffset in the + // batchUpdate completion block, but it caused a distinct flicker, which I was able to avoid with the + // `CollectionViewLayout.prepare` based approach. + mediaTileViewLayout.isInsertingCellsToTop = true + mediaTileViewLayout.contentSizeBeforeInsertingToTop = collectionView.contentSize collectionView.performBatchUpdates({ mediaGalleryDataSource.ensureGalleryItemsLoaded(.before, item: oldestLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections) items: \(addedItems)") @@ -362,12 +370,6 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe collectionView.insertItems(at: addedItems) } }, completion: { finished in - - // Adjust content offset to affect change in content height so that the same content is visible after - // the update. - let newContentOffset = CGPoint(x: 0, y: collectionView.contentSize.height - scrollDistanceToBottom) - collectionView.setContentOffset(newContentOffset, animated: false) - Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)") self.isFetchingMoreData = false CATransaction.commit() @@ -424,7 +426,33 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe } } -class MediaGallerySectionHeader: UICollectionReusableView { +// MARK: - Private Helper Classes + +// Accomodates remaining scrolled to the same "apparent" position when new content is insterted +// into the top of a collectionView. There are multiple ways to solve this problem, but this +// is the only one which avoided a perceptible flicker. +fileprivate class MediaTileViewLayout: UICollectionViewFlowLayout { + + fileprivate var isInsertingCellsToTop: Bool = false + fileprivate var contentSizeBeforeInsertingToTop: CGSize? + + override public func prepare() { + super.prepare() + + if isInsertingCellsToTop { + if let collectionView = collectionView, let oldContentSize = contentSizeBeforeInsertingToTop { + let newContentSize = collectionViewContentSize + let contentOffsetY = collectionView.contentOffset.y + (newContentSize.height - oldContentSize.height) + let newOffset = CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY) + collectionView.setContentOffset(newOffset, animated: false) + } + contentSizeBeforeInsertingToTop = nil + isInsertingCellsToTop = false + } + } +} + +fileprivate class MediaGallerySectionHeader: UICollectionReusableView { static let reuseIdentifier = "MediaGallerySectionHeader" @@ -486,11 +514,11 @@ class MediaGallerySectionHeader: UICollectionReusableView { } } -public protocol MediaGalleryCellDelegate: class { +fileprivate protocol MediaGalleryCellDelegate: class { func didTapCell(_ cell: MediaGalleryCell, item: MediaGalleryItem) } -public class MediaGalleryLoadingHeader: UICollectionViewCell { +fileprivate class MediaGalleryLoadingHeader: UICollectionViewCell { static let reuseIdentifier = "MediaGalleryLoadingHeader" @@ -519,7 +547,7 @@ public class MediaGalleryLoadingHeader: UICollectionViewCell { } } -public class MediaGalleryCell: UICollectionViewCell { +fileprivate class MediaGalleryCell: UICollectionViewCell { static let reuseIdentifier = "MediaGalleryCell"