iphone_duo_ui_pro 0.1.0
iphone_duo_ui_pro: ^0.1.0 copied to clipboard
Fold-aware Flutter widgets for iPhone Duo: the hinge as a DisplayFeature, vertical bars, size classes, two-pane layouts and fold-aligned grids, fed by a native iOS bridge.
On iPhone Duo a Flutter app resizes itself correctly, and MediaQuery.paddingOf even reports the
asymmetric safe area. The rest is missing: MediaQuery.displayFeatures stays empty on iOS, so
nothing in Dart knows where the fold is, and your navigation stays pinned to the bottom while the
system moves its own bars to the side of the screen.
This package fills that gap. A native bridge reports what UIKit and SwiftUI already know — reserved regions, size classes, the hinge, the vertical bar edge — and a small set of widgets turn that state into layout.
flutter pub add iphone_duo_ui_pro
What you get #
| API | What it does |
|---|---|
DuoScope |
Streams the native bridge into the widget tree as a DuoEnvironment. |
DuoDisplayFeatures |
Republishes the fold and the under-display camera as DisplayFeatures, so every system route avoids them on its own. |
FoldAwareTwoPane |
List and detail split exactly on the fold, one pane at compact width. |
DuoAdaptiveScaffold |
Moves navigation and actions into the vertical bar when the display calls for it, and keeps content state across the switch. |
FoldAlignedGrid, duoEvenColumns |
Tile grids whose columns stay symmetric around the hinge. |
showDuoDialog, showDuoModalBottomSheet, duoAnchorPoint |
Presentations anchored to the region Apple's guidance asks for. |
Setup #
Requires Flutter 3.38+ and an app on the UIScene life cycle — apps built against the iOS 27 SDK without it do not launch at all. Minimum deployment target is iOS 13; every iPhone Duo API is guarded by availability checks.
MaterialApp(
builder: (context, child) => DuoScope(
child: DuoDisplayFeatures(child: child ?? const SizedBox.shrink()),
),
home: const HomeScreen(),
);
That is the whole installation. The plugin registers itself through Flutter's
GeneratedPluginRegistrant, so there is no app-side Swift to write. On platforms without the bridge
DuoScope reports DuoEnvironment.unavailable and every widget falls back to its plain layout, so
the same code keeps working on a regular iPhone, on Android and in tests.
Two panes, and dialogs that step aside #
FoldAwareTwoPane(
startPane: SessionList(onSelected: _select),
endPane: SessionDetail(id: _selected),
);
showDuoDialog<void>(
context: context,
builder: (context) => const AlertDialog(title: Text('Delete this session?')),
);
showDuoModalBottomSheet<void>(
context: context,
purpose: DuoAnchorPurpose.controls, // the lower region in the tabletop pose
builder: (context) => const PlaybackControls(),
);
The anchor point is deliberately constant — a window corner rather than a measured fold. Dialog
routes recompute DisplayFeatureSubScreen on every MediaQuery change, so a dialog opened while
the device is flat moves to the correct half the moment it is folded. A point computed from the
current fold would be captured by the route and end up in the wrong half.
The vertical bar #
On the outer display, and on the inner display in landscape, the system moves toolbars, navigation
and tab bars to the side. DuoAdaptiveScaffold follows: the same actions, in the order Apple
specifies, with an icon and a label on each so they read identically in the overflow menu.
DuoAdaptiveScaffold(
title: 'Library',
prominentAction: DuoBarAction(icon: Icons.add, label: 'Add', onPressed: _add),
topActions: <DuoBarAction>[
DuoBarAction(
icon: Icons.edit_outlined,
label: 'Compose',
priority: DuoVisibilityPriority.high,
onPressed: _compose,
),
DuoBarAction(icon: Icons.inbox_outlined, label: 'Inbox', badgeCount: 7, onPressed: _inbox),
],
tabs: const <DuoTab>[
DuoTab(icon: Icons.photo_library_outlined, label: 'Library'),
DuoTab(icon: Icons.grid_view_outlined, label: 'Grid'),
],
selectedTabIndex: _tab,
onTabSelected: (index) => setState(() => _tab = index),
body: content,
);
Two details worth knowing. A floating action button is only shown in the horizontal layout — in the
vertical bar its role is taken by prominentAction, which is why the scaffold asserts you passed
one. And the body travels between layouts under a GlobalKey, so scroll offsets and text fields
survive opening, closing and rotating the device.
Grids aligned to the hinge #
An even column count is the usual advice, and it is enough when the content area is centred on the
fold. Add a vertical bar on one side and the middle gap misses the hinge. FoldAlignedGrid plans
columns outwards from the centre of the fold instead, so folding the device grows the middle gap and
changes nothing else — not the column count, not the outer margins.
FoldAlignedGrid(
itemCount: photos.length,
tileHeight: 104,
minTileWidth: 120,
itemBuilder: (context, index) => PhotoTile(photos[index]),
);
What the bridge reports #
DuoScope.of(context) returns a DuoEnvironment:
| Member | Meaning |
|---|---|
isAvailable, sdk271 |
Whether the bridge answered, and whether it was built with the iOS 27.1 APIs. |
size, horizontalSizeClass, verticalSizeClass |
The UIKit view size and size classes — on iPhone Duo width alone cannot tell you the class. |
divisions, occlusions |
Reserved regions: folds (active and inactive) and the camera under the display. |
hasDivisionInView, activeFold |
Whether a fold is in this view at all, and the one currently splitting it. |
isBookPose, isTabletopPose |
Which way the device is folded. |
hinge |
Hinge status and angle, or null on a device without one. |
verticalBarEdgeKnown, barEdgeFor(direction) |
Which physical edge the system put its bars on. |
iOS 27.1 and the DUO_SDK_27_1 flag #
Reserved regions, the vertical bar edge and the hinge ship with the iOS 27.1 SDK. Until every
build machine has Xcode 27.1, those call sites stay behind a compilation condition — #available
alone is not enough, because the symbols do not exist in the 27.0 SDK and the compiler stops at
"cannot find … in scope". Turn them on in your ios/Podfile:
post_install do |installer|
installer.pods_project.targets.each do |target|
next unless target.name == 'iphone_duo_ui_pro'
target.build_configurations.each do |config|
config.build_settings['SWIFT_ACTIVE_COMPILATION_CONDITIONS'] = '$(inherited) DUO_SDK_27_1'
end
end
end
Without the flag the bridge still reports size classes and the view size, so DuoAdaptiveScaffold
and FoldAwareTwoPane keep working; folds simply never appear. On Android, displayFeatures comes
from Flutter itself, so FoldAlignedGrid works there with no bridge at all.
Honest status: the 27.1 call sites are transcribed from Apple's iPhone Duo Tech Talks and are
marked VERIFY(27.1 SDK) in the source. They have not been compiled against a shipping 27.1 SDK
yet. Everything else — layout, bar planning, anchors, grids — is covered by the test suite and runs
today.
Testing without the device #
Pass an environment and the channel is never touched:
const fold = DuoRegion(rect: Rect.fromLTWH(467.5, 0, 16, 669), active: true);
await tester.pumpWidget(
DuoScope(
environment: const DuoEnvironment(
isAvailable: true,
sdk271: true,
size: Size(951, 669),
horizontalSizeClass: DuoSizeClass.regular,
verticalSizeClass: DuoSizeClass.regular,
divisions: <DuoRegion>[fold],
hinge: DuoHinge(status: DuoHingeStatus.partiallyOpen, angleDegrees: 120),
),
child: const DuoDisplayFeatures(child: MyApp()),
),
);
The package ships 28 tests built this way, covering both poses, RTL, Split View halves, overflow priorities and state preservation across layout switches.
Configurations at a glance #
| Configuration | Size classes | Bars | Panes |
|---|---|---|---|
| Outer display, portrait | compact × regular | vertical | one |
| Outer display, landscape | compact × compact | vertical | one |
| Inner display, portrait | regular × regular | horizontal | two, 38 / 62 |
| Inner display, landscape | regular × regular | vertical | two |
| Half open (book pose) | regular × regular | as above | split on the fold |
Screen sizes for the record: the outer display is 466 × 678 pt, the inner one 669 × 951 pt — derived from the App Store Connect screenshot specification, useful for previews and tests, never for hard-coding layout.
Sources #
Built from Apple's iPhone Duo material: the six Tech Talks (Get ready for iPhone Duo, Strike a pose with adaptive layouts, Raise the bar, Leverage multiple displays and scenes, Update your camera app, Design for iPhone Duo) and the Designing for iPhone Duo Human Interface Guidelines.
Author #
Szymon Dziedzic — mobile developer, Flutter and native iOS. LinkedIn · szymondziedzic.com
License #
MIT — see LICENSE.