Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bk/target content offset improvements #116

Merged
merged 3 commits into from
Feb 13, 2024

Conversation

bryankeller
Copy link
Contributor

Details

This further improves our scroll position preservation logic. I've gone down a funny rabbit hole trying to solve this, discovering https://github.com/ekazaev/ChatLayout and the related blog post https://itnext.io/my-covid-19-lockdown-project-or-how-i-started-to-dig-into-a-custom-uicollectionviewlayout-to-get-a-d053e1ad3aa0 - this project referenced a ton of MagazineLayout code, but also took things a step further for scroll position preservation since it's a key requirement of a chat interface. This blog post has some solid information about the complications of using self-sizing cells with targetContentOffset.

The main benefit to this PR is that we now handle some self-sizing-cell edge cases that previously caused some jumps.

Related Issue

N/A

Motivation and Context

Improve scroll position preservation logic

How Has This Been Tested

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

@bryankeller bryankeller added the bug Something isn't working label Feb 10, 2024
Comment on lines +25 to +26
case topItem(id: String, itemEdge: ItemEdge, distanceFromTop: CGFloat)
case bottomItem(id: String, itemEdge: ItemEdge, distanceFromBottom: CGFloat)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change allows us to preserve the scroll position more accurately when cells change size on rotation (their height might grow / shrink depending on the width after rotation)

Comment on lines +266 to +272
if let stagedContentOffsetAdjustment {
let context = MagazineLayoutInvalidationContext()
context.invalidateLayoutMetrics = false
context.contentOffsetAdjustment = stagedContentOffsetAdjustment
invalidateLayout(with: context)
}
stagedContentOffsetAdjustment = nil
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@brynbodayle brynbodayle Feb 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm seeing it, maybe we should add this in a comment? Curious why we invalidate the layout here.

Comment on lines +761 to +780
} else if
verticalLayoutDirection == .bottomToTop,
!isSizingElementBelowBottomEdge,
modelState.isPerformingBatchUpdates
{
contentOffsetAdjustment = CGPoint(
x: 0,
y: preferredAttributes.size.height - originalAttributes.size.height)
} else {
contentOffsetAdjustment = .zero
}

if modelState.isPerformingBatchUpdates {
// If we're in the middle of a batch update, we need to adjust our content offset. Doing it
// here in the middle of a batch update gets ignored for some reason. Instead, we delay
// slightly and do it in `finalizeCollectionViewUpdates`.
// This needs to be set in `finalizeCollectionViewUpdates`, otherwise it doesn't get applied.
stagedContentOffsetAdjustment = contentOffsetAdjustment
} else if isScrolling {
context.contentOffsetAdjustment = contentOffsetAdjustment
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code was already barely intelligible trash, and this certainly didn't help. It works though, and handles the cases where self-sizing items are inserted at the top / bottom

Comment on lines -767 to -785
let metricsSnapshot = MetricsSnapshot(
bounds: currentCollectionView.bounds,
contentSize: collectionViewContentSize,
contentInset: lastContentInset ?? contentInset)

// Replace existing snapshot if the bounds is the same
let indexOfExistingMetricsSnapshot = metricsSnapshots.firstIndex {
$0.bounds == metricsSnapshot.bounds
}
if let indexOfExistingMetricsSnapshot {
metricsSnapshots[indexOfExistingMetricsSnapshot] = metricsSnapshot
} else {
metricsSnapshots.append(metricsSnapshot)
}

// Limit to 3 snapshots
if metricsSnapshots.count > 3 {
metricsSnapshots.removeFirst()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need any of this historical tracking of layout metrics, since we simply no longer take into account content insets on rotation

visibleItemLocationFramePairs.append(itemLocationFramePair)
}
visibleItemLocationFramePairs.sort { $0.elementLocation < $1.elementLocation }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there were some bugs because this array wasn't guaranteed to be sorted before - whoops!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good find!

contentHeight: preInvalidationContentSize?.height ?? currentCollectionView.contentSize.height,
// There doesn't seem to be a reliable way to get the correct content insets here. We can try
// track them in invalidateLayout or prepare, but then there are edge cases where we need to
// track multiple past inset values. It's a huge mess, and I don't think it's worth solving.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

brynbodayle
brynbodayle previously approved these changes Feb 10, 2024
@bryankeller bryankeller force-pushed the bk/target-content-offset-improvements branch from bcef59d to fe53e8c Compare February 13, 2024 19:26
@bryankeller bryankeller merged commit e70f47b into master Feb 13, 2024
2 checks passed
@bryankeller bryankeller deleted the bk/target-content-offset-improvements branch February 13, 2024 19:30
!isSizingElementBelowBottomEdge,
modelState.isPerformingBatchUpdates
{
contentOffsetAdjustment = CGPoint(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like we don't need to do this content offset adjustment for an onscreen cell, otherwise we get content offset jumps

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simulator.Screen.Recording.-.iPhone.15.-.2024-02-14.at.14.28.57.mp4

} else if
verticalLayoutDirection == .bottomToTop,
!isSizingElementBelowBottomEdge,
modelState.isPerformingBatchUpdates

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing in testing that we need this content offset even when batch updates aren't being performed. Here's a video of the below scenario where removing this check prevented a content offset jump. It's when you insert a cell at the top and it's offscreen.

Simulator.Screen.Recording.-.iPhone.15.-.2024-02-14.at.14.47.36.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants