Skip to content

Add DisplayFeatures plugin: size class, pose, and reserved regions (iOS, iPhone Duo) - #8597

Draft
saqibkayani2077 wants to merge 2 commits into
ionic-team:nextfrom
saqibkayani2077:iphone-duo-support
Draft

saqibkayani2077 wants to merge 2 commits into
ionic-team:nextfrom
saqibkayani2077:iphone-duo-support

Conversation

@saqibkayani2077

Copy link
Copy Markdown

What

Adds a DisplayFeatures plugin exposing native display metadata that web content can't read from CSS alone: active size class, device pose, and reserved-region frames (hinge/camera) — starting with iPhone Duo, Apple's first foldable iPhone, announced September 9, 2026.

WKWebView already resizes with the window and exposes env(safe-area-inset-*) to CSS, so basic layout adaptation on iPhone Duo works without this plugin. This is for the cases JS needs to decide something CSS can't express: which size class is active (the HIG's primary outer/inner-display signal), whether the device is partially folded, and where the folding region or an active camera region sits.

Why

The reserved-region/pose model follows Apple's Tech Talk 111463 ("Strike a pose with adaptive layouts on iPhone Duo") and the Designing for iPhone Duo HIG.

Current limitation — please read before reviewing

The iOS 27.1 SDK that ships the underlying reserved-region APIs (UIView.reservedRegions(kind:)) is not publicly available yet — Apple lists Xcode 27.1 beta as "coming later this month" as of 2026-09-10. So today, getDisplayFeatures() honestly reports supported: false and never guesses at pose/region data. Size classes, safe-area insets, and bounds are real values on every iOS device today. The native DisplayFeaturesProvider protocol is structured so a real implementation can be dropped in once the SDK ships, without changing the public API.

The Swift side is unverified — I don't have Xcode in the environment I used to write this, so DisplayFeatures.swift has never been compiled. Everything else has:

Test plan

npm run test (ts-jest) from core/ — all 5 tests pass:

DisplayFeatures
  ✓ getDisplayFeatures proxies to the native implementation and returns its result untouched
  ✓ addListener on native returns a removable handle for displayFeaturesChanged
  ✓ delivers displayFeaturesChanged state to listeners
  ✓ rejects as unavailable on web
  ✓ rejects as unimplemented on a native platform without the plugin

ts-jest type-checks as part of running, so the TS API surface is type-verified, not just written.

Disclosure

This PR was written with AI assistance (Claude Code), directed and reviewed by me. Happy to answer questions, adjust the approach, or close this if a different direction (e.g. extending an existing plugin, or waiting for the SDK to be public) is preferred.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LAkF81dVyenmpEChH2Nh1k

WKWebView already gets safe-area CSS and resizes with the window, so
basic iPhone Duo layout needs no plugin. This exposes what CSS can't:
active size class (the HIG's primary outer/inner signal), device pose,
and reserved-region frames (camera/fold), via getDisplayFeatures() and
a displayFeaturesChanged listener.

Pose/region data comes from a DisplayFeaturesProvider. The default
UnsupportedDisplayFeaturesProvider reports supported: false and never
guesses, because the iOS 27.1 SDK that ships the reserved-region APIs
(per Apple Tech Talk 111463) is not public yet - Apple lists Xcode 27.1
beta as "coming later this month" as of 2026-09-10. Size classes,
safe-area insets and bounds are real values on every iOS device today.
Android is unimplemented (rejects as Unimplemented).

Verified: `npm run test` (ts-jest, from core/) - all 5 tests pass:
  DisplayFeatures
    ✓ getDisplayFeatures proxies to the native implementation and returns its result untouched
    ✓ addListener on native returns a removable handle for displayFeaturesChanged
    ✓ delivers displayFeaturesChanged state to listeners
    ✓ rejects as unavailable on web
    ✓ rejects as unimplemented on a native platform without the plugin
ts-jest type-checks as part of running, so the TS API surface (core-plugins.ts,
index.ts) is now type-verified, not just written. UNVERIFIED: the Swift
plugin itself - no Xcode in this environment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAkF81dVyenmpEChH2Nh1k
@theproducer theproducer self-assigned this Sep 10, 2026
@theproducer

Copy link
Copy Markdown
Contributor

This is intriguing! I haven't looked this over in detail yet, but have you thought about adding Android support for similar display features?

@saqibkayani2077

Copy link
Copy Markdown
Author

Great suggestion, Joey! Android is actually in a much better position than iOS here: androidx.window (Jetpack WindowManager) already ships FoldingFeature — state (FLAT/HALF_OPENED), orientation, occlusionType, and bounds — mature, stable, and in production on real foldables (Galaxy Z Fold, Surface Duo, etc.), unlike the iOS side in this PR, which depends on an iOS 27.1 SDK Apple hasn't publicly released yet.

I'm going to add real Android support mapping FoldingFeature onto the same DisplayFeaturesState shape this PR already defines (currently {supported, pose, activeDisplay, horizontalSizeClass, verticalSizeClass, reservedRegions, safeAreaInsets, bounds} — some of those iOS-specific fields would need rethinking for a shared cross-platform shape, will think through that). Unlike the iOS half, I can actually compile and test this against the real SDK, so it won't carry the same caveats. Will follow up here once it's ready.

Following up on review feedback on this PR (theproducer/Joey Pender):
adds the Android side of the DisplayFeatures plugin, backed by
androidx.window's FoldingFeature API - stable since 2021 and in
production on real foldables (Samsung Galaxy Z Fold, and, while it
shipped, the Microsoft Surface Duo). Unlike the iOS half of this PR,
this is fully compiled and tested against the real SDK, not just
written.

Cross-platform shape reconciliation: DisplayFeaturesState was designed
around iPhone Duo's two-named-displays/UIKit-trait model, so several
fields have no honest Android equivalent:
- activeDisplay ('inner'/'outer'): no Android analogue for a single
  Activity's window - always 'unknown'.
- horizontalSizeClass/verticalSizeClass: a UIKit trait-collection
  concept. Android has an analogous but differently-shaped system
  (androidx.window.core.layout.WindowSizeClass: three tiers, width and
  height reported separately) that's deliberately out of scope here to
  keep this change focused on fold detection - always 'unspecified'.
- reservedRegions/pose reuse the existing shape as-is: a separating
  FoldingFeature maps to a 'fold'-type region and 'partiallyOpen'
  (HALF_OPENED) or 'open' (FLAT) pose. A non-separating fold (e.g. some
  tabletop postures) isn't reported as a region, since it isn't meant to
  divide content. No FoldingFeature at all reports 'unknown' pose rather
  than guessing 'open', since WindowLayoutInfo can't distinguish "flat,
  nothing to report" from "not a foldable device" from "on hardware this
  API doesn't cover".
- safeAreaInsets/bounds are computed for real via WindowInsetsCompat and
  the WebView's measured size, mirroring how SystemBars already computes
  safe-area insets on this codebase.

getDisplayFeatures() registers a one-shot WindowInfoTracker listener and
resolves on its first callback rather than reading a cached value, so it
can't race with plugin/WebView startup ordering. The persistent listener
from handleOnStart() drives displayFeaturesChanged separately.

Verified for real, not just written:
`./gradlew clean lint build test -b capacitor/build.gradle` (the exact
command this repo's own CI runs) - BUILD SUCCESSFUL. Lint: no new issues
against the existing baseline. Both testDebugUnitTest and
testReleaseUnitTest pass, including 5 new DisplayFeaturesTest cases.

Two real bugs found only by actually compiling/running this against the
SDK, not by review:
1. WindowInfoTrackerCallbackAdapter's listener methods take
   androidx.core.util.Consumer, not java.util.function.Consumer - a
   real javac error, not a style choice.
2. Rect.width()/height() throw "not mocked" under Android's unit-test
   stub jar; rewrote to use (right - left)/(bottom - top) field
   arithmetic instead, which is unaffected and equally correct. Also
   found the stub jar's Rect 4-arg constructor doesn't reliably persist
   field values either - the test fixture builds Rects via direct field
   assignment instead of the constructor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAkF81dVyenmpEChH2Nh1k
@saqibkayani2077

Copy link
Copy Markdown
Author

Android support is up. A few notes on how it turned out:

This one is fully compiled and tested against the real SDK (unlike the iOS half, which I flagged as unverified) — ./gradlew clean lint build test -b capacitor/build.gradle (the exact command this repo's own CI runs) is green: lint clean against the baseline, both debug and release unit tests pass, including 5 new test cases.

Backed by androidx.window's FoldingFeature — stable since 2021, used in production on real foldables. It reuses the existing reservedRegions/pose fields rather than adding new API surface. A couple of fields genuinely don't have an Android equivalent (activeDisplay, the two size-class fields — those are iPhone-Duo/UIKit-specific concepts), so those report 'unknown'/'unspecified' on Android rather than guessing at something. Full reasoning is in the commit message.

Two real bugs only showed up once I actually compiled it — WindowInfoTrackerCallbackAdapter wants androidx.core.util.Consumer, not java.util.function.Consumer, and Rect.width()/height() throw under the unit-test stub jar (fixed with field arithmetic instead). Neither would've been caught by review alone.

@erkamyaman

erkamyaman commented Sep 18, 2026 •

Copy link
Copy Markdown

For anyone who needs it today, @erkamyaman/capacitor-foldable (npm) covers all of this for Capacitor 7 and 8.

  • iOS: iPhone Duo support on the real iOS 27.1 SDK. Built with Xcode 27.1 and tested on the iPhone Duo simulator: closed, book, tabletop and fully open, in every rotation.
  • Android: Jetpack WindowManager, tested on Galaxy Z Fold8, Z Flip8 and Z Fold8 Ultra.
  • Web: a Device Posture and Viewport Segments polyfill, so plain CSS reacts to the fold.

It reports fold state, posture, hinge position and angle, size classes, and where iPhone Duo moves native bars, with change events for each.

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.

3 participants