Skip to content

[HOLD for #1234] fix(ios): animate setPage when the pager resizes with the keyboard - #1224

Open
TaduJR wants to merge 1 commit into
callstack:mainfrom
TaduJR:fix/ios-animate-setpage-after-blur
Open

TaduJR wants to merge 1 commit into
callstack:mainfrom
TaduJR:fix/ios-animate-setpage-after-blur

Conversation

@TaduJR

@TaduJR TaduJR commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #1223.

An imperative setPage stopped animating when the pager resizes with the keyboard. #1193 resigns the input when the pager leaves a page, so the keyboard closes and a pager sized around it springs back to full height about 17 ms later, and SwiftUI drops the slide for a selection applied before that layout lands.

The fix leaves the blur where it is and lets the layout land before changing the selection. Commands arriving together share that wait, so the pager animates once, to the page asked for last. A command naming the page already selected writes the selection and returns without waiting, so it cannot undo a swipe committing meanwhile, and a waiting selection is clamped if its page is removed.

The wait is two main-queue hops, which lasts as long as the main thread is busy: 21 ms while the keyboard closes, so the relayout at 17 ms lands first. It does not cover a resize the app starts in the same handler as the command, where the hops finish in under a millisecond and a relayout 13 ms later still kills the slide. Waiting for Core Animation's commit with a run-loop observer measured the same, so this keeps the simpler version.

Measured by counting scroll frames per command, where a slide reports about 18 and a jump 1: with the pager inside a KeyboardAvoidingView, three keyboard-up moves report 1, 1, 1 on main and 18, 18, 18 with this change. Removing the blur from setPage on main restores the slide too, which is where the regression comes from.

That measurement was taken by hand, with the repro screen's root wrapped in a KeyboardAvoidingView. Turning it into a test also needs the screen's Events: line to stop reflowing, and with that fixed height a separate race shows up instead, where a redirect ends on the earlier page in 11 of 23 runs on main. So the test waits on that race rather than on this change. The strand the fixed height also exposed is fixed in #1234.

Every animated setPage now waits, so onPageSelected arrives correspondingly later.

Test Plan

What's required for testing (prerequisites)?

bun run bootstrap
bun run example:ios:release

The keyboard tests need the simulator to show its own keyboard, which a device that has seen a Mac keyboard will not. Clear both flags on every booted device after building (also in e2e/README.md):

for udid in $(xcrun simctl list devices booted | grep -Eo '[0-9A-F-]{36}'); do
  xcrun simctl spawn "$udid" defaults write com.apple.keyboard.preferences HardwareKeyboardLastSeen -bool false
  xcrun simctl spawn "$udid" defaults write com.apple.keyboard.preferences AutomaticMinimizationEnabled -bool false
done

What are the steps to reproduce (after prerequisites)?

On Issue #1154 Page Change Focus Repro: tap the input on page 0 and then Go 1, then the input on page 1 and Go 2. That second move used to swap pages with no slide. On this screen the resize comes from the event log reflowing rather than from the keyboard, which is the same mechanism by another route.

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

The new case moves twice with the keyboard open and checks both moves slid by reading the Scrolls: N counter the repro shows: a jump reads Scrolls: 1. On an iPhone 18 Pro simulator, iOS 27, React Native 0.88, the file passes 14 of 14, twice over.

The SwiftPM job here fails a setPage that interrupts another still lands on the later page, which is #1230 and not caused by this change. A fix for it is open as #1234, and this PR should go green once that merges.

Compatibility

OS Implemented
iOS ✅
Android ❌

Checklist

  • I have tested this 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

// always enough. The wait does not depend on the blur above finding an
// input, since a keyboard already on its way out collides the same way.
props.pendingPage = index
DispatchQueue.main.async { [props] in

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

q: Why are there 2 DispatchQueue.main calls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One hop isn't enough in practice. Counting scroll frames per command on a release build, where a slide reports about 18 and a jump 1, with the keyboard open:

  • one hop: slide dropped in 5 of 20 runs, always the second keyboard-up move
  • two hops: 1 of 50

I also tried waiting for the Core Animation commit explicitly, with a one-shot CFRunLoopObserver on .beforeWaiting: also 1 of 50, for about 18 more lines, so I kept the simpler version.

It isn't a commit boundary, incidentally logging the order gives setPage -> hop1 -> hop2 -> commit. The second hop just yields once more so the keyboard dismissal and its layout drain before the selection changes.

We can switch to the observer, or to put these numbers in the comment if you would like.

@TaduJR

TaduJR commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

I think the CI failures are infra

@troZee

Could you re-run them?

Thanks!

@TaduJR TaduJR changed the title fix(ios): animate setPage when the keyboard is open fix(ios): animate setPage when the pager resizes with the keyboard Nov 5, 2026
@TaduJR

TaduJR commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Of the three CI fail on of them isn't infra. It fails a setPage that interrupts another still lands on the later page, which is #1230: a setPage that interrupts another can leave the pager stopped between pages, with the selection and the scroll position disagreeing and the input on the page being left still focused. It reproduces on main, so re-running this job is a won't work until #1230 is fixed.

@TaduJR TaduJR changed the title fix(ios): animate setPage when the pager resizes with the keyboard [HOLD #1234]fix(ios): animate setPage when the pager resizes with the keyboard Nov 6, 2026
@TaduJR TaduJR changed the title [HOLD #1234]fix(ios): animate setPage when the pager resizes with the keyboard [HOLD for #1234] fix(ios): animate setPage when the pager resizes with the keyboard Nov 6, 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.

[iOS] setPage does not animate when the pager resizes with the keyboard

2 participants