Skip to content

fix(ios): keep a setPage sent during a slide - #1238

Draft
KisaneNeko wants to merge 1 commit into
callstack:mainfrom
KisaneNeko:fix/ios-redirect-reverts
Draft

KisaneNeko wants to merge 1 commit into
callstack:mainfrom
KisaneNeko:fix/ios-redirect-reverts

Conversation

@KisaneNeko

@KisaneNeko KisaneNeko commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A setPage sent while a slide is still running can end on the wrong page on iOS. This is the cause of the iOS e2e flakes in issue-1154-page-change-focus.e2e.ts that remain after #1234.

While a slide runs, TabView writes the page nearest the offset into the selection on each scroll frame. If such a frame comes between the new selection and TabView's update, the new page is lost. Two tests send a second command right after the first: a setPage that interrupts another still lands on the later page and a setPage redirected once the focus has landed blurs it. In CI that command often arrives while the first slide is still running. The pager then stops on page 1 instead of page 2 (selected 2, selected 1). On a fast machine TabView applies the selection first, which is why these tests do not fail locally.

There is a second case of the same behaviour. TabView starts no scroll for a selection that is already the page nearest the offset. Take a slide from 0 to 2 that is redirected to 1 past the middle of page 1. It runs on to page 2, and TabView writes 2 back (selected 2, selected 1, selected 2, state idle at 2).

The fix:

  • Before a setPage changes the selection, the pager stops a running slide where it is. TabView then has no slide to take a page from, and it scrolls to the new selection.
  • After the command, the pager checks for a skipped scroll. If TabView started no scroll and the pager is nearest the new page, the pager scrolls there itself.
  • The pager records where an animated scroll is heading, which UIKit does not expose. Both steps use it. The stop acts only on a programmatic slide. When a slide is already heading to the page, the second step does nothing. Without that check, a slow main thread let the second step replace a normal slide, which made the flake worse.

A slide that a finger drives or that decelerates after a swipe is left alone. iOS 15 is left alone too.

Test Plan

What's required for testing (prerequisites)?

bun run bootstrap
bun run example:ios:release

What are the steps to reproduce (after prerequisites)?

On Issue #1154 Page Change Focus Repro, tap Go 2, back to 1. It calls setPage(2), then calls setPage(1) from onPageScroll once the slide is more than halfway to page 1. Expected: the pager stops on page 1. On main it stops on page 2.

The other case needs a slow main thread, so it shows on CI and not on a fast machine.

cd e2e && bun run test:ios --tag issue-1154
cd e2e && bun run test:ios

CI, iOS 26.4 simulator, both iOS jobs, the two flaky tests 20 times each with no retries (a temporary workflow change, since removed):

Code Failed
main with #1234 36 of 80
This change 0 of 80

With native traces from the same runs: on main, 35 of 35 commands sent mid-slide ended on the earlier page, and 44 of 44 sent after the slide were fine. With this change the stop acted in 30 runs, and all of them passed.

Locally, iPhone 17 Pro simulator, iOS 26.4, release build:

  • a setPage back to the page a slide is passing lands there fails 10 of 10 without the change and passes 10 of 10 with it.
  • The redirect tests in that file pass 29 of 30 with the change. The one failure is a LOCATOR_NOT_FOUND before any page change.
  • The full iOS suite passes 44 of 46. The failures are TabView inside a ScrollView, where the tab tap sends no setPage to the pager, and one run of the search bar inset test.

Not covered: a real device, iOS versions other than 26.4, vertical paging and right to left for these redirects, and Android, which this does not touch.

Compatibility

OS Implemented
iOS ✅
Android ❌

Checklist

  • Tested on a device and a simulator (simulator only)
  • I added the documentation in README.md (no documented behaviour changes)
  • I updated the typed files, TS and Flow (no API change)

@krozniata

Copy link
Copy Markdown
Member

@KisaneNeko Is there anything that needs to be done in this PR?

Would love to get it merged to address flaky test 🙏

@KisaneNeko

Copy link
Copy Markdown
Collaborator Author

@krozniata i kept it as draft because #1234 had part of the fix, since it was merged yesterday I'll sync with main and test if the CI is back to green.

@KisaneNeko
KisaneNeko force-pushed the fix/ios-redirect-reverts branch from df12cef to 0bfa289 Compare November 13, 2026 06:23
@KisaneNeko

Copy link
Copy Markdown
Collaborator Author

@krozniata the part that I thought would be fixed with #1234 are still broken :( back to the drawing board I'll try to address it

@KisaneNeko
KisaneNeko force-pushed the fix/ios-redirect-reverts branch from 8fe9d19 to 647aab6 Compare November 13, 2026 11:46
While a slide runs, TabView writes the page nearest the offset into the
selection, which can overwrite a setPage sent mid-slide. Stop the running
slide before changing the selection.

TabView also starts no scroll for a selection that is already the page
nearest the offset, so a slide redirected back to the page it is passing
runs on to its old target. Start that scroll from the pager, unless a
slide is already heading there.
@KisaneNeko
KisaneNeko force-pushed the fix/ios-redirect-reverts branch from 84cf145 to cb4af6f Compare November 13, 2026 15:17
@KisaneNeko KisaneNeko changed the title fix(ios): scroll to a setPage that TabView skips mid-slide fix(ios): keep a setPage sent during a slide Nov 13, 2026

This branch has not been deployed

No deployments
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.

2 participants