Skip to content

fix(zero-swift): line up clamped jumps, re-pick the anchor row, and page onto the screen above a permalink - #81

Merged
tmm merged 1 commit into
mainfrom
swift-virtual-jump-anchor-fixes
Nov 3, 2026
Merged

tmm merged 1 commit into
mainfrom
swift-virtual-jump-anchor-fixes

Conversation

@tmm

@tmm tmm commented Nov 3, 2026

Copy link
Copy Markdown
Member

Stacked on #80. Both change VirtualizerCore around the same paths (carrySpace, land, afterUpdate), so this PR targets #80's branch, swift-virtual-bottom-fixes. Merge #80 first, and GitHub retargets this one to main.

Three more cases where VirtualList and VirtualListView were left wrong until the next scroll, on iOS and macOS, from the same bug hunt. Two of them (4 and 5) are deliberate divergences from upstream, which behaves the same way; UPSTREAM.md records both and why.

4. A clamped jump to a loaded row is never lined up. scrollToItem to a loaded row near the end of the loaded rows, with nothing seen below them, landed short. For example, .center on the second-to-last loaded row put it at 355 of a 400pt viewport. It stayed there when the rows below loaded.

  • Cause: there's no space after the loaded rows, so the scroll is clamped at their end. land releases a jump once its window settles, clamped or not, so paging isn't held up. Nothing scrolls into the room the rows loading below make.
  • Fix: the jump is still released, so paging carries on. A jump clamped short of the list's own end is kept as a realign request: as rows load, it scrolls on to its row until the row lines up, the list's real end clamps it, or the user scrolls.
  • Upstream: #land at f89c468 releases it the same way. We diverge because the row the app asked for should end up where it asked.

5. The anchor row goes stale after paging up into space. After a fast scroll up past the loaded rows, a row on screen changing height (an image loading, text reflowing) moved the rows above it instead of the ones below.

  • Cause: manual anchoring picks its reference row only at a scroll. Scrolled into the space above the rows, that's the window's first row, below the viewport, and the page loading above pushes it down the screen.
  • Fix: when the rows are in place, measureAndCompensate picks the reference again: the first row reaching below the viewport's top, as native scroll anchoring picks it on each layout.
  • Upstream: #refreshAnchor has the same timing. We diverge because native anchoring, which this stands in for, re-picks every layout.

6. A blank screen scrolling up past the rows of a list opened at a permalink. Scrolled up past the loaded rows in one go, the screen stayed blank while the list loaded pages back to its start and forward again. On a real VirtualListView it was 10 blank frames, and the list ended at its first row.

  • Cause: the page before the loaded rows shares none of them, since a backward page's start row is exclusive. carrySpace found no row in common and left the space as it was: the reserve of space a list opened away from its start keeps above its rows. That put the new page where the old rows were, below the viewport.
  • Fix: a page that shares no row with the last one now goes where the space estimated its rows, a row's estimate each from the space as it was, not from the reserve. Jumps and restores, which number their rows anew and own the position while landing, are left out.
  • Options: I also considered dropping the reserve. That would change how a permalink opens (much less content above it, and a different scroll indicator) and make rows loading above it compensate with scrolls that stop a fling. This fix doesn't change how a permalink opens.

Tests. Each new LayoutHostTests test fails without the fix:

  • "a jump to a loaded row near the end of the loaded rows lines it up once rows load below"
  • "after scrolling up past the loaded rows, a row on screen growing moves only the rows below it"
  • "scrolled up past the loaded rows of a list opened at a permalink, the next page is on screen"

Verified locally:

  • swift test on macOS: everything passes, including the fling tests (flingKeepsGoing, flingsUpFromAPermalink).
  • 4 and 6 on a real VirtualListView and on the SwiftUI VirtualList on macOS, with a scratch test. Without the fix the row lands at 355 instead of 200, and the list goes blank and ends at its start.
  • A seeded randomized run over both PRs (300 seeds).

It found one rare, older failure that this PR doesn't address. A window at the list's start ends above the viewport and the page after it starts below it, so paging switches between them without settling. It shows on #80's tree too.

Base automatically changed from swift-virtual-bottom-fixes to main November 3, 2026 07:24
…age onto the screen above a permalink

Three more cases where VirtualList and VirtualListView were left wrong
until the next scroll, on iOS and macOS. Stacked on the fix for content
shrinking at the bottom, which changes the same code in VirtualizerCore.

- A jump to a loaded row near the end of the loaded rows, with nothing
  seen below them, was clamped there and released, and the row was never
  lined up as rows loaded below. A jump clamped short of the list's own
  end now scrolls on to its row as rows load, until it lines up, the
  list's end clamps it, or the user scrolls. It's still released, so
  paging carries on. Upstream releases it; UPSTREAM.md records the
  divergence.
- The reference row manual anchoring keeps in place was picked only at
  a scroll. After a scroll up into the space above the loaded rows, that
  was their first row, below the viewport, and the page loading above
  pushed it down the screen, so a row above it changing height moved
  the rows above it. It's now picked again whenever the rows are in
  place, as native anchoring does. Upstream picks it only at a scroll;
  UPSTREAM.md records the divergence.
- Scrolled up past the rows of a list opened at a permalink, the page
  before them shares none of their rows, so carrySpace kept the space
  as it was: the reserve such a list keeps above its rows, which put the
  page where the old rows were, below the viewport. The list paged on to
  its start and back, blank. A page that shares no row with the last one
  now goes where the space estimated its rows, from the space as it was.
  How a permalink opens is unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-01a19fc4-5fb6-711a-b937-d8c57793372f
Co-authored-by: tmm <tmm@tmm.dev>
@tmm
tmm force-pushed the swift-virtual-jump-anchor-fixes branch from 03c688c to d957eba Compare November 3, 2026 07:34
@tmm
tmm merged commit 74dcab8 into main Nov 3, 2026
25 checks passed
@tmm
tmm deleted the swift-virtual-jump-anchor-fixes branch November 3, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant