diff --git a/Signal/src/ViewControllers/MediaTileViewController.swift b/Signal/src/ViewControllers/MediaTileViewController.swift index e8f7aeb2c..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 @@ -340,15 +343,25 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe return } + guard !mediaGalleryDataSource.hasFetchedOldest else { + return + } + guard !isFetchingMoreData else { Logger.debug("\(logTag) in \(#function) already fetching more data") return } - 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)") @@ -357,15 +370,11 @@ 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() }) + } else if oldContentHeight - contentOffsetY < kEdgeThreshold { // Near the bottom, load newer content @@ -374,22 +383,31 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryCe return } + guard !mediaGalleryDataSource.hasFetchedMostRecent else { + return + } + guard !isFetchingMoreData else { Logger.debug("\(logTag) in \(#function) already fetching more data") return } - isFetchingMoreData = true - collectionView.performBatchUpdates({ - mediaGalleryDataSource.ensureGalleryItemsLoaded(.after, item: mostRecentLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in - Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections), items: \(addedItems)") - collectionView.insertSections(addedSections) - collectionView.insertItems(at: addedItems) - } - }, completion: { finished in - Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)") - self.isFetchingMoreData = false - }) + + CATransaction.begin() + CATransaction.setDisableActions(true) + UIView.performWithoutAnimation { + collectionView.performBatchUpdates({ + mediaGalleryDataSource.ensureGalleryItemsLoaded(.after, item: mostRecentLoadedItem, amount: self.kMediaTileViewLoadBatchSize) { addedSections, addedItems in + Logger.debug("\(self.logTag) in \(#function) insertingSections: \(addedSections), items: \(addedItems)") + collectionView.insertSections(addedSections) + collectionView.insertItems(at: addedItems) + } + }, completion: { finished in + Logger.debug("\(self.logTag) in \(#function) performBatchUpdates finished: \(finished)") + self.isFetchingMoreData = false + CATransaction.commit() + }) + } } } @@ -408,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" @@ -470,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" @@ -503,7 +547,7 @@ public class MediaGalleryLoadingHeader: UICollectionViewCell { } } -public class MediaGalleryCell: UICollectionViewCell { +fileprivate class MediaGalleryCell: UICollectionViewCell { static let reuseIdentifier = "MediaGalleryCell"