Modify layout to handle performBatchUpdates().

This commit is contained in:
Matthew Chen 2020-12-16 10:12:07 -03:00
parent e5fe14cd32
commit 2a480d24dc
6 changed files with 125 additions and 68 deletions

View File

@ -528,17 +528,25 @@ public class CVLoadCoordinator: NSObject {
let (loadPromise, loadResolver) = Promise<Void>.pending()
var attemptCount = 0
let viewState = self.viewState
func canLoad() -> Bool {
!viewState.isScrollingToTop && !viewState.isUserScrolling
func canLandLoad() -> Bool {
let canLandLoad = !viewState.hasScrollingAnimation && !viewState.isUserScrolling
attemptCount += 1
Logger.verbose("attemptCount: \(attemptCount), canLandLoad: \(canLandLoad), hasScrollingAnimation: \(viewState.hasScrollingAnimation), isUserScrolling: \(viewState.isUserScrolling), ")
return canLandLoad
// !viewState.hasScrollingAnimation && !viewState.isUserScrolling
}
func tryToResolve() {
guard canLoad() else {
guard canLandLoad() else {
// TODO: async() or asyncAfter()?
Logger.verbose("Waiting to land load.")
// DispatchQueue.asyncAfter() will take longer to perform
// its block than DispatchQueue.async() if the CPU is under
// heavy load. That's desirable in this case.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
// DispatchQueue.main.async {
tryToResolve()
}
return
@ -573,31 +581,6 @@ public class CVLoadCoordinator: NSObject {
return loadPromise
}
// public func waitUntilCanLandLoad() -> Promise<Void> {
// AssertIsOnMainThread()
//
// guard let canLandLoadPromise = canLandLoadPromise else {
// return Promise.value(())
// }
// return canLandLoadPromise.promise
// }
// private func tryToFireCanLandLoad() {
// AssertIsOnMainThread()
//
// guard let canLandLoadPromise = canLandLoadPromise else {
// return
// }
// guard !isScrollingToTop else {
// return
// }
// canLandLoadPromise.resolver.fulfill(())
// self.canLandLoadPromise = nil
// }
// private struct PromiseAndResolver {
// let promise: Promise<Void>
// let resolver: Resolver<Void>
// }
// private var canLandLoadPromise: PromiseAndResolver?
// -
@ -872,6 +855,10 @@ extension CVLoadCoordinator: UIScrollViewDelegate {
public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
delegate?.scrollViewDidScrollToTop?(scrollView)
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
delegate?.scrollViewDidEndScrollingAnimation?(scrollView)
}
}
// MARK: -

View File

@ -64,7 +64,9 @@ public class CVViewState: NSObject {
@objc
public var isUserScrolling = false
@objc
public var isScrollingToTop = false
public var scrollingAnimationStartDate: Date?
@objc
public var hasScrollingAnimation: Bool { scrollingAnimationStartDate != nil }
@objc
public var scrollContinuity: ScrollContinuity = .bottom
public var scrollContinuityMap: CVScrollContinuityMap?
@ -202,11 +204,13 @@ public extension ConversationViewController {
set { viewState.isUserScrolling = newValue }
}
var isScrollingToTop: Bool {
get { viewState.isScrollingToTop }
set { viewState.isScrollingToTop = newValue }
var scrollingAnimationStartDate: Date? {
get { viewState.scrollingAnimationStartDate }
set { viewState.scrollingAnimationStartDate = newValue }
}
var hasScrollingAnimation: Bool { viewState.hasScrollingAnimation }
var scrollContinuity: ScrollContinuity {
get { viewState.scrollContinuity }
set { viewState.scrollContinuity = newValue }

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)collectionViewWillChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize;
- (void)collectionViewDidChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize;
- (void)collectionViewWillAnimate;
@end

View File

@ -64,6 +64,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if (animated) {
[self.layoutDelegate collectionViewWillAnimate];
}
[super setContentOffset:contentOffset animated:animated];
}
@ -97,6 +101,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated
{
if (animated) {
[self.layoutDelegate collectionViewWillAnimate];
}
[super scrollRectToVisible:rect animated:animated];
}

View File

@ -936,6 +936,9 @@ typedef enum : NSUInteger {
- (void)readTimerDidFire
{
if (self.layout.isPerformingBatchUpdates || self.hasScrollingAnimation) {
return;
}
[self markVisibleMessagesAsRead];
}
@ -3173,10 +3176,15 @@ typedef enum : NSUInteger {
{
self.userHasScrolled = YES;
self.isUserScrolling = YES;
[self scrollingAnimationDidStart];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)willDecelerate
{
if (!willDecelerate) {
[self scrollingAnimationDidComplete];
}
if (!self.isUserScrolling) {
return;
}
@ -3192,6 +3200,8 @@ typedef enum : NSUInteger {
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self scrollingAnimationDidComplete];
if (!self.isWaitingForDeceleration) {
return;
}
@ -3203,24 +3213,74 @@ typedef enum : NSUInteger {
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
self.isScrollingToTop = YES;
// isScrollingToTop blocks landing of loads, so we must ensure
// that it is always cleared in a timely way, even if the animation
// is cancelled. Wait no more than 2 seconds.
__weak ConversationViewController *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)2.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
// isScrollingToTop should already have been cleared...
OWSAssertDebug(!weakSelf.isScrollingToTop);
// ...but we want to make sure.
weakSelf.isScrollingToTop = NO;
});
[self scrollingAnimationDidStart];
return YES;
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
self.isScrollingToTop = NO;
[self scrollingAnimationDidComplete];
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[self scrollingAnimationDidComplete];
}
#pragma mark - ConversationCollectionViewDelegate
- (void)collectionViewWillChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
OWSAssertIsOnMainThread();
}
- (void)collectionViewDidChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
OWSAssertIsOnMainThread();
if (oldSize.width != newSize.width) {
[self resetForSizeOrOrientationChange];
}
}
- (void)collectionViewWillAnimate
{
[self scrollingAnimationDidStart];
}
- (void)scrollingAnimationDidStart
{
OWSAssertIsOnMainThread();
NSDate *startDate = [NSDate new];
self.scrollingAnimationStartDate = startDate;
// scrollingAnimationStartDate blocks landing of loads, so we must ensure
// that it is always cleared in a timely way, even if the animation
// is cancelled. Wait no more than N seconds.
__weak ConversationViewController *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)5.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
ConversationViewController *_Nullable strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// scrollingAnimationStartDate should already have been cleared,
// but we need to ensure that it is cleared in a timely way.
if ([NSObject isNullableObject:strongSelf.scrollingAnimationStartDate equalTo:startDate]) {
OWSFailDebug(@"Scrolling animation did not complete in a timely way.");
[strongSelf scrollingAnimationDidComplete];
}
});
}
- (void)scrollingAnimationDidComplete
{
OWSAssertIsOnMainThread();
self.scrollingAnimationStartDate = nil;
[self autoLoadMoreIfNecessary];
}
@ -3549,22 +3609,6 @@ typedef enum : NSUInteger {
}
}
#pragma mark - ConversationCollectionViewDelegate
- (void)collectionViewWillChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
OWSAssertIsOnMainThread();
}
- (void)collectionViewDidChangeSizeFrom:(CGSize)oldSize to:(CGSize)newSize
{
OWSAssertIsOnMainThread();
if (oldSize.width != newSize.width) {
[self resetForSizeOrOrientationChange];
}
}
#pragma mark - ContactsPickerDelegate
- (void)contactsPickerDidCancel:(ContactsPicker *)contactsPicker

View File

@ -380,7 +380,8 @@ public class ConversationViewLayout: UICollectionViewLayout {
// the initial (last) layout state for items.
private var lastLayoutInfo: LayoutInfo?
private var isPerformingBatchUpdates = false
@objc
public var isPerformingBatchUpdates = false
private var hasInvalidatedDataSourceCounts = false
@objc
@ -447,20 +448,32 @@ public class ConversationViewLayout: UICollectionViewLayout {
super.finalizeLayoutTransition()
}
// MARK: -
// A layout can return the content offset to be applied during transition or update animations.
public override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
return proposedContentOffset
guard isPerformingBatchUpdates else {
return proposedContentOffset
}
guard let delegate = delegate else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity)
}
return delegate.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
// A layout can return the content offset to be applied during transition or update animations.
public override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return proposedContentOffset
guard isPerformingBatchUpdates else {
return proposedContentOffset
}
guard let delegate = delegate else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
return delegate.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
private var initialLayoutInfo: LayoutInfo? {
lastLayoutInfo
}
private var finalLayoutInfo: LayoutInfo {
ensureCurrentLayoutInfo()