Skip to content

[iPhone Duo] Proposal: populate MediaQuery.displayFeatures on iOS for iPhone Duo (foldable) #192515

Description

@mirzaaghazadeh

Use case

Apple announced iPhone Duo on September 9, 2026 — the first foldable iPhone, with a 5.4" outer display and a 7.6" inner display that curves through the centre when partially folded. It ships October 23, 2026.

Flutter apps run on it today, but they have no fold awareness at all:

  • DisplayFeature is documented as "populated only on Android." On iPhone Duo, MediaQuery.displayFeatures returns an empty list no matter how folded the device is.
  • dual_screen (TwoPane, hingeAngleEvents) is Android-only in practice and was last published roughly three years ago.

So a Flutter app cannot tell that content or a touch target has landed in the fold — where, on this device, it becomes noticeably harder to read and to tap.

Some of the impact is already solvable in pure Dart, and I don't think Flutter needs to do anything about that part: resizing is handled by LayoutBuilder, and the asymmetric safe area (iOS moves its system controls to one side on this device) is correctly reported through MediaQuery.paddingOf per edge. That covers a good portion of a decent port.

What is not reachable from Dart is the fold itself, and the under-display camera region.

Alternatives considered. Each app can write its own platform channel to the new iOS APIs. That works — but it means every foldable-aware Flutter app reimplements the same bridge, and none of them feed MediaQuery.displayFeatures, so no existing display-feature-aware widget or package benefits. Given DisplayFeature already exists as the platform-neutral abstraction, populating it on iOS seems strictly better than N private channels.

Related: #189554 proposes built-in split-view/dual-pane support for large screens and foldables. This issue is narrower — just getting the platform data into dart:ui.

Proposal

Populate DisplayFeature on iOS from the reserved-region APIs introduced in iOS 27.1, in the Darwin embedder (engine/src/flutter/shell/platform/darwin/ios/), plumbed through ViewportMetrics the same way the Android embedder does it from Jetpack WindowManager.

iOS 27.1 adds reservedRegions(kind:) on UIView, returning regions with a frame, a kind, and an active/inactive state:

  • .division — an area that splits a larger area into smaller ones. The fold is a division region. It is active only while the device is folded; flat, it is inactive with zero width.
  • .occlusion — an area that covers part of the view without splitting it. The under-display FaceTime camera on the inner display is an occlusion region, active only while that camera is in use.
  • A query option includeInactive returns regions that exist but are not currently active.

A mapping onto the existing model that I think holds up:

iOS 27.1 reserved region DisplayFeatureType Notes
.division (the fold) fold Flutter defines fold as not obstructing the display and hinge as obstructing. iPhone Duo's inner display is a single continuous panel that curves — there is no gap and no second panel — so fold is the accurate value and hinge would be misleading.
.occlusion (under-display camera) cutout Occludes without dividing, which matches cutout semantics.
iOS region state DisplayFeatureState
active postureHalfOpened
inactive (zero width when flat) postureFlat

bounds maps directly from the region's frame, converted to logical pixels.

One open design question

Flutter has no equivalent of includeInactive, and I think that gap is worth deciding on deliberately rather than by omission.

On iOS you can ask "does this device have a fold, even though it is flat right now?" That is genuinely useful for stable layout decisions — Apple's guidance gives the example of always preferring an even number of grid columns on a device that has a fold, so the layout does not reshuffle every time the user bends it.

Flutter's model cannot express that: an inactive feature simply is not in the list, so displayFeatures is empty on a flat iPhone Duo and equally empty on an iPhone 18. An app that wants to lay out stably across the fold cannot distinguish them.

Possible directions, roughly in order of invasiveness:

  1. Report the inactive fold as a DisplayFeature with state: postureFlat and zero-width bounds — arguably already consistent with postureFlat existing at all, and requires no API change.
  2. Add an opt-in accessor for inactive features.
  3. Leave it out for now, and document that displayFeatures reflects only currently-active features.

Option 1 looks cheapest and most useful to me, but it is a behavioural decision for the team — it would mean a flat foldable reports a feature where today it would report none, and that could surprise Android callers.

Caveat on my end

I have not been able to verify these iOS API signatures against the SDK. iPhone Duo was announced on September 9 and Xcode 27.1 was still rolling out at the time of writing, so everything above comes from Apple's Tech Talk sessions rather than shipped headers. I would treat the names as the shape of the API and confirm against the SDK before implementing.

I am filing this as a proposal rather than a PR for that reason — I cannot build or test the engine change, so I did not want to submit code I could not verify. Happy to help with documentation or follow-up detail if that is useful.

Primary sources (Apple):

Activity

  1. added
    in triagePresently being triaged by the triage team
    platform-iosiOS applications specifically
    engineflutter/engine related. See also e: labels.
    c: proposalA detailed proposal for a change to Flutter
    team-iosOwned by iOS platform team
    and removed
    in triagePresently being triaged by the triage team
    on Sep 10, 2026
  2. diegolopezrm commented on Sep 10, 2026

    @diegolopezrm
    Contributor

    Useful proposal, and the caveat about not verifying against the SDK is the right call — I checked and none of UIHingeInteraction, reservedRegion, includeInactive or UIFold exist in the iOS 26.1 SDK's UIKit headers, so those names can't be confirmed until the 27.x toolchain is out.

    I did measure the other half of it: what the framework does once displayFeatures is non-empty on iOS. Three things came out of it, one of which is a bug in the mapping table.

    The open question answers itself, but not the way it's framed. Option 1 — inactive fold as zero-width bounds plus postureFlat — is behaviourally inert. DisplayFeatureSubScreen.avoidBounds returns empty and an AlertDialog stays full width at Rect.fromLTRB(0, 0, 800, 900). So reporting an inactive fold is safe, and the "could surprise Android callers" worry doesn't materialise.

    The decision that does change behaviour isn't active-vs-inactive. It's whether the embedder reports non-zero .division bounds at all, because the predicate is:

    // display_feature_sub_screen.dart:137
    d.bounds.shortestSide > 0 || d.state == DisplayFeatureState.postureHalfOpened

    With a 40pt fold at the centre of an 800x900 view:

    AlertDialog          800 -> 380 wide
    modal bottom sheet   800 -> 380
    DatePickerDialog     800 -> 380
    popup menu item      x=388 -> x=272
    

    Eleven files under packages/flutter/lib/src/ inherit that one predicate, including a hand-duplicated copy in cupertino/menu_anchor.dart. So the day iOS reports a division, every dialog, sheet and picker on the 7.6" inner display gets confined to one ~47% half, with anchorPoint as the only escape hatch. That seems worth deciding on deliberately rather than discovering.

    Worth flagging the degenerate case too: zero-width bounds with postureHalfOpened passes the predicate and yields avoidBounds of Rect.fromLTRB(400, 0, 400, 900) — a 400pt dialog and a silent half-screen split with no visible fold. That's the trap if an embedder reasons "device is folded, therefore postureHalfOpened" while Apple reports the flat division as zero-width.

    The mapping table as written would assert-crash. It maps .occlusion to DisplayFeatureType.cutout while applying the active -> postureHalfOpened rule, but:

    // platform_dispatcher.dart:2676
    assert(!identical(type, DisplayFeatureType.cutout) ||
           identical(state, DisplayFeatureState.unknown));

    A cutout has to carry DisplayFeatureState.unknown, so occlusions need to skip the state rule.

    One semantic note. Flutter's split model comes from Android hinges, where two panels genuinely face different directions and treating them as separate sub-screens is right. The Duo's inner display is one continuous curved panel, and Apple's guidance is about not placing content in the division rather than about treating the halves as separate screens. Those aren't the same thing, and DisplayFeatureSubScreen implements the first one.

    Numbers above are from a small widget-test harness against framework master — happy to push it somewhere if it's useful for whoever picks this up, and to re-run it once 27.1 is out and the real reserved-region values are known.

  3. added
    P2Important issues not at the top of the work list
    on Sep 10, 2026
  4. changed the title [-]Proposal: populate MediaQuery.displayFeatures on iOS for iPhone Duo (foldable)[/-] [+][iPhone Duo] Proposal: populate MediaQuery.displayFeatures on iOS for iPhone Duo (foldable)[/+] on Sep 19, 2026
  5. hellohuanlin commented on Sep 19, 2026

    @hellohuanlin
    Contributor

    The reserved region can also be active/inactive for front camera of inner display, which has nothing to do with DisplayFeatureState. Reusing DisplayFeatureState (folded / open / half open) for a completely separate concept (active / inactive) can be confusing. We will need to update the API to include this info.

  6. hellohuanlin commented on Sep 20, 2026

    @hellohuanlin
    Contributor

    Another issue is how to tell if reserved region has changed for front camera of inner display (turn it on/off)

  7. hellohuanlin commented on Sep 20, 2026

    @hellohuanlin
    Contributor

    Also an inactive region should still report a non-zero size, so that apps will know how to prepare for it. Passing an inactive region with zero frame would unlikely be any useful.

  8. berkaycatak commented on Sep 20, 2026

    @berkaycatak

    I opened #193025 for this. It populates displayFeatures on iOS from UIHingeInteraction and the reserved regions, and is ready for review.

    Until it lands, the foldable package exposes the same data through a platform channel, plus the hinge angle and posture, which are outside the DisplayFeature model.

  9. diegolopezrm commented on Sep 21, 2026

    @diegolopezrm
    Contributor

    On telling when a reserved region changes: I couldn't find an API for it either. Apple is running a written UIKit Q&A for iPhone Duo on the 23rd and that question is already on my list — I'll bring back whatever they answer.

    On active/inactive: agreed it doesn't belong in DisplayFeatureState. Worth adding that the two aren't even in step — in #193025 isActive lags the hinge status, so a division can still read active in the update that reports the device fully open. Whatever ends up carrying active/inactive has to tolerate that skew instead of assuming the two agree.

  10. JamesFieldist commented on Nov 11, 2026

    @JamesFieldist

    Tracking this from Fieldist for iPhone Duo unfold detection (https://github.com/Fieldist/fieldist/issues/2941).

    MediaQuery.displayFeatures is empty on iOS today, so we match the inner Duo logical size (669×951) as a stand-in. Flutter 3.50 (#195199 / #195690) can change that size. We will drop the size branch and unify on fold/hinge features when this issue and PR #193025 land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

P2Important issues not at the top of the work listc: proposalA detailed proposal for a change to Fluttere: device-specificOnly manifests on certain devicesengineflutter/engine related. See also e: labels.platform-iosiOS applications specificallyteam-iosOwned by iOS platform teamtriaged-iosTriaged by iOS platform team

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions