Skip to content

fix(theme): start the page transition before native suspension settles - #292

Open
phiamo wants to merge 1 commit into
rdlabo-dev:mainfrom
phiamo:fix/native-transition-overlap
Open

phiamo wants to merge 1 commit into
rdlabo-dev:mainfrom
phiamo:fix/native-transition-overlap

Conversation

@phiamo

@phiamo phiamo commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Summary

withNativeUIShellTransition currently awaits prepare() (the suspend) before play(). That retires the native controls while the page is still at rest, so Web shows for a frame before the slide starts. On a real iPhone SE (iOS 26) this is visible as a flash of the header segment or back button on tap.

This starts prepare() and play() together and awaits prepare() before finish(). Nothing else changes: Native UI, the bridge and Swift are untouched. The change is in the theme, which owns transition coordination since #282.

Follow-up to the discussion in #266 (closed). Anything beyond this, such as visibility, measurement or handoff duration, is deliberately not included.

Behavior change

  • play() is now called before the suspension settles.
  • A destroy() during preparation can now happen after play() has already started. The test "destroy during bridge preparation cannot start an abandoned transition" had to change from ['destroy', 'resume:true'] to ['play', 'destroy', 'resume:true']. Is that acceptable, or should an abandoned transition be guarded another way?

Validation

  • native-ui-shell-transition.spec.ts: 11 passed (play starts before suspension settles; destroy case updated).
  • build:ts and Prettier pass on the changed files.
  • npm run test:esm and the demo Sass build fail on current main as well (missing utils/structured-list), with or without this change. I could not run the full demo or browser suites for that reason.
  • Device: the tap-time flash was verified on an iPhone SE against an equivalent change on the previous flat layout. I have not re-run it on this exact packages/ revision.

Co-authored-by: Cursor <cursoragent@cursor.com>

@rdlabo rdlabo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for investigating the tap-time flash, testing it on an iPhone SE, and keeping the change focused. The contribution is very much appreciated, and calling out the changed destroy behavior in the PR description was especially helpful.

The motivation makes sense: today, suspension restores the Web controls before the slide starts, leaving an interval in which the static Web header can become visible. However, the current ordering also protects the transition lifecycle. Moving the wait into finally does not preserve that protection: Ionic determines transition completion through onFinish, rather than awaiting the promise returned by play(), and destroys the animation after completion.

I compared the original implementation and this PR in Chromium using real Ionic Web Animations and a deliberately delayed suspension. Two regressions reproduced around packages/theme-ios27/src/transition/native-ui.ts:61–67:

  1. A successful transition can resume as canceled. With a 20 ms animation and a 200 ms suspension, reproducing Ionic's finish-then-destroy lifecycle produced step = 1 in both versions, but the original called resume(false) and this PR called resume(true). When suspension eventually settles, disposed is already true. For Vertical Bars, the canceled path marks the entering page as departed, even though navigation completed successfully.
  2. Destroying during preparation can leave play() pending. The original has not started playback yet and returns after preparation completes. This PR has already started playback; destroying the animation removes its completion callbacks, leaving the awaited playback promise unresolved. In the browser comparison, it remained pending beyond the animation's configured duration, although the suspension lease itself was released.

The existing 11 tests pass, but the mock play() resolves immediately and does not exercise this destruction behavior with Web Animations enabled.

I would keep suspension-before-play ordering for now and address the visible handoff gap while preserving that guarantee. If overlapping the two operations is necessary, successful completion and cancellation need to remain distinct through late suspension and teardown, with browser regression coverage for both cases above. The flash is worth fixing; this particular ordering change needs those lifecycle issues resolved before merging.

@rdlabo

rdlabo commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Thank you again for investigating the flash and proposing this change. Your contribution prompted a useful look at the presentation model itself, which I have summarized in #296 with simulator recordings.

There may be a more fundamental direction to explore before changing the suspend/play ordering: keeping the native bar present while its items transition between pages, instead of restoring Web controls and projecting them again around each navigation. In a UIKit-only reference app, standard UINavigationController push/pop keeps native bar presentation while the page moves and the button background changes width between “Edit” and “Finish editing.”

This is not yet a working Ionic integration. A standalone UINavigationBar did not reproduce that animation in our fixture, and interactive synchronization/action ownership remain open questions. The issue separates those limitations from the verified behavior.

My inclination is to investigate that direction first, since it may remove the handoff responsible for the flash rather than shorten its visible interval. The completion/cancellation and pending-play concerns in my earlier review still apply to this patch. This does not take away from the contribution—the flash you identified is real, and this investigation builds on it. There is no expectation that you take on the architectural work in this PR.

@phiamo

phiamo commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks a lot for going deeper into this, can i help you in any way?

@rdlabo

rdlabo commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Thank you for offering to help, and for identifying the flash in the first place.

The replacement approach discussed in #296 is now implemented in #315 and released in v2.0.1. Supported toolbar controls remain native during navigation, with button correspondence and progress synchronization, while retaining the preparation-before-play ordering and cancellation safeguards we discussed here.

Could you try 2.0.1 on your iPhone SE and let us know whether it resolves the flash you reported and matches the behavior you intended with this PR? Please update the packages, rebuild the Web assets, run npx cap sync, and rebuild the native app so the native changes are included. Your feedback on that device would be very helpful.

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