Skip to content

feature: OrientationChanged event + System::orientation() — react to device rotation from PHP - #371

Open
SRWieZ wants to merge 2 commits into
NativePHP:mainfrom
SRWieZ:feat/orientation-changed-event
Open

feature: OrientationChanged event + System::orientation() — react to device rotation from PHP#371
SRWieZ wants to merge 2 commits into
NativePHP:mainfrom
SRWieZ:feat/orientation-changed-event

Conversation

@SRWieZ

@SRWieZ SRWieZ commented Aug 24, 2026

Copy link
Copy Markdown
Member

What this does

When the app window changes between portrait and landscape, PHP is notified, so a screen can render a different layout for each orientation.

Until now, an app that allows both orientations would reflow natively when its window changed, but the PHP side never knew it happened. There was no way to say "in landscape, put the score tiles side by side".

How you use it

React to an orientation change in any screen:

use Native\Mobile\Events\System\OrientationChanged;

#[On(OrientationChanged::class)]
public function rotated(string $orientation): void
{
    // 'portrait' or 'landscape' — state changed, screen re-renders
}

Or ask at any time:

System::orientation();  // 'portrait' | 'landscape'
System::isLandscape();  // bool
System::isPortrait();   // bool

How it works

It follows the existing AppearanceChanged pattern end to end:

  • Native pushes an event when the app window's aspect changes.
    • Android emits from onConfigurationChanged. The last orientation lives in the companion object, so an activity recreation caused by multi-window resizing compares the fresh configuration in onCreate() and emits when PHP's process cache would otherwise remain stale.
    • iOS watches layout changes but reads the actual app-window bounds for both push and query paths. Keyboard safe-area changes therefore cannot create false iPad orientation events. It uses the current iOS 17+ onChange form.
  • A listener in NativeServiceProvider keeps System's cached value fresh, so reads avoid another bridge round-trip after the first one.
  • System.GetOrientation backs the first read, before any orientation event has arrived.

Good to know

  • Orientation describes the app window, not necessarily the physical device. They can differ in iPad and Android multi-window modes.
  • The event only fires if the app enables a second orientation in config/nativephp.php.
  • The payload is deliberately minimal: 'portrait' | 'landscape'.
  • Because the event implements BroadcastsGlobally, it also reaches app-wide Event::listen() listeners.
  • Android webview screens depend on #379 to route native events into PHP.
  • The documentation site needs a follow-up orientation page; it does not live in this repository.

Tests

  • Orientation event/global-dispatch and cache behaviour.
  • Invalid-value guard and native-payload rebuild.
  • Explicit service-provider listener coverage for both orientation and appearance events.

Full suite: 902 passed. Pint and PHPStan clean.

…ation from PHP

Mirrors the AppearanceChanged pattern end to end: native pushes an
OrientationChanged event ('portrait' | 'landscape') when the device
rotates, a NativeServiceProvider listener keeps System's process cache
fresh, and System.GetOrientation backs the cold read before the first
push. Android emits from onConfigurationChanged (the manifest already
routes rotation there); iOS derives orientation from the window aspect
so it tracks what layout actually sees. Only fires when the app enables
a second orientation in config/nativephp.php.

Claude-Session: https://claude.ai/code/session_015kkVBULWwHPzWccgz4zLVe
@SRWieZ SRWieZ changed the title feat: OrientationChanged event + System::orientation() — react to device rotation from PHP feature: OrientationChanged event + System::orientation() — react to device rotation from PHP Aug 24, 2026

@gwleuverink gwleuverink 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.

Great addition, this one was missing. Verified on an Android emulator and the iOS simulator: works as described on EDGE screens, one event per rotation, clean cold read, no false positives on theme flips.

Two cases where it goes wrong, both measured:

iPad keyboard. With ipad => true, focusing a text field in portrait emits landscape, hiding the keyboard emits portrait. The GeometryReader measures the safe-area frame, which the keyboard shrinks below square on an iPad (1024x1366 → 1024x963); GetOrientation reads window.bounds, so the two disagree while the keyboard is up, and a rotation with the keyboard open leaves System::orientation() stuck on landscape. Read the window (or interfaceOrientation) on both sides, or put .ignoresSafeArea(.keyboard) on the reader. iPhone is unaffected.

Android multi-window. A resize that changes smallestScreenSize or screenLayout recreates the activity instead of calling onConfigurationChanged (the manifest only declares uiMode|colorMode|orientation|screenSize), so nothing is pushed and onCreate re-seeds lastOrientation silently. PHP keeps the stale value for the rest of the process. Keeping lastOrientation in the companion object and emitting from onCreate when the fresh read differs covers it.

Smaller:

  • Docblock: both platforms measure the window aspect, not the device. Worth saying, and the Swift GetOrientation comment "same signal the push uses" is not true.
  • .onChange(of:) { size in } is the iOS 17-deprecated form (target is 18.2; the colorScheme twin has it too).
  • Nothing covers the provider listener: deleting the Event::listen leaves the suite green for both events.
  • Docs need a follow-up, orientation has no page yet.

Merge after #379: on Android a rotation on a webview screen never reaches PHP (the #360 gap, inherited from AppearanceChanged) and the cache then stays wrong process-wide. #379 routes those events through /_native/api/events, which dispatches through the provider listener, so that closes on its own.

@gwleuverink gwleuverink 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.

Perfect 🙌🏻

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