Skip to content

feat(native-ui): opt-in atomic swap handoff for stack push/pop (supersedes #266) - #291

Closed
phiamo wants to merge 1 commit into
rdlabo-dev:mainfrom
phiamo:fix/native-swap-handoff
Closed

phiamo wants to merge 1 commit into
rdlabo-dev:mainfrom
phiamo:fix/native-swap-handoff

Conversation

@phiamo

@phiamo phiamo commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Supersedes #266 (closed). Rebased onto the new packages/ layout and trimmed. It answers the questions asked there.

Problem

On a real iPhone SE (iOS 26), native header chrome (a segment that turns into a back button) flashes Web for a frame on stack push and pop. This is visible on a plain Ionic nested stack with nothing exotic.

Why suspend() / resume() is not enough

  1. connectNativeUIShellTransition awaits prepare() (the suspend) before play(). Native chrome is therefore retired while the page is still at rest, and Web shows for a frame before the slide starts. This is the tap-time flicker.
  2. Native overlays do not ride the ion-page transform. During the slide, visible() / blockedReason see the entering page as transformed or hidden and retire its chrome. Only the leaving chrome should retire.
  3. The handoff duration alone is not enough. I tried duration 0 first (that was feat(native): opt-in instant Web/native swap on stack push/pop #266's original commit) and it still flickered on device.

Keeping the entering controls native through the slide is a requirement for us: the glass segment and back button are the product, and a Web fallback for 350ms looks broken next to them.

Change

  • theme-ios27: withNativeUIShellTransition overlaps prepare() and play() and awaits prepare() before finish(). This affects all transitions that use the adapter.
  • Everything else is gated on data-shell-handoff="swap" (or the ios-theme-shell-handoff-swap class):
    • the entering swap page is measured at rest (swapEnteringPage, withoutPageTransform) and stays projected;
    • measureMarker is used while measuring so the shell marker is not cleared;
    • leftPages stops a resumed leaving page from re-projecting;
    • duration is 0 and the two-frame paint wait is skipped only when every changed source is a swap. Mixed sets keep 180ms.
  • Docs: the handoff section of native-ui-shell.md.
  • Tests: new dom-atomic-swap.spec.ts, and native-ui-shell-transition.spec.ts now asserts that play starts before suspend completes.

Without the attribute, tab switches, modals, menus and unchanged-chrome pushes behave as before. The only unconditional behavior change is the play/prepare overlap.

Verification

  • Unit tests: the affected demo specs pass (52). Typecheck and build:ts pass. Prettier is clean on the changed files.
  • On device: iPhone SE (iOS 26), real catalog stack, push and pop for course, media special, session and search open/close. No Web flash. This was verified on an equivalent port against the previous flat layout. I have not re-run it on this exact packages/ revision, and the Playwright e2e is not included because I couldn't run it here. The demo Sass build currently fails on utils/structured-list on main.

If you would prefer to split the play/prepare overlap into its own PR first, I can do that.

Overlap suspend with the page slide and let swap-marked chrome hand off at rest
with duration 0, so native header controls do not flash Web on push/pop.

Refs rdlabo-dev#266

Co-authored-by: Cursor <cursoragent@cursor.com>
@phiamo phiamo closed this Oct 18, 2026
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