Skip to content
Mobile Apps

Flutter on iPhone Duo and iOS 27: upgrade playbook

Flutter is ready for the iOS 27 scene life cycle, but its foldable API is empty on iPhone Duo. What to check, what breaks, and a plugin design for the fold.

Ingenious Techlab TeamUpdated 12 min read
Contents
  1. Part 1: iOS 27 and the scene life cycle
  2. Part 2: iPhone Duo layout
  3. Part 3: the fold, and Flutter’s empty displayFeatures
  4. A plan, in order

Flutter enters this autumn in an unusual position. Of the major cross-platform frameworks it is the best prepared for iOS 27 and has the most frustrating gap on iPhone Duo.

  • iOS 27: apps built with the iOS 27 SDK must use UIKit’s scene-based life cycle or they fail to launch. Flutter already ships that support, and migrates projects with an unmodified app delegate automatically.
  • iPhone Duo: Flutter already has a well-designed API for foldable screens, built for Android — and on iOS it is always empty.

This is a playbook for both. Status is as of the date at the top; the linked Flutter issues are active.

Part 1: iOS 27 and the scene life cycle

Scene life cycle support by stack, checked 2026-09-20
StackStatusWhat it means for you
UIKit / SwiftUI (native)You migrateAdopt a scene manifest and a window scene delegate. SwiftUI apps using the App protocol already use scenes.TN3187
FlutterShipped in 3.41Default since 3.41; unmodified AppDelegates migrate automatically on build. Custom AppDelegates, and older Flutter versions, migrate by hand.Flutter migration guide
React Native0.88 release candidateScene life cycle support, including linking, is in the 0.88 release candidates — 0.88.0-rc.1 is current — and in no stable release. 0.88 stable is scheduled for 12 October. On 0.87 or earlier, add a SceneDelegate to your iOS project yourself.Commit · 0.88 on npm · Release schedule · Template PR for 0.87
ExpoShipped, opt-in on 57SDK 58 beta uses scenes by default. On SDK 57 the runtime shipped too, but you opt in: set ios.enableSceneSupport in expo-build-properties and run expo 57.0.23 or newer. SDK 56 still fails to launch under Xcode 27.SDK 58 beta · enableSceneSupport · SDK 57 runtime · Original issue
Status of the scene life cycle requirement by stack, checked on . This changes weekly — follow the links for the current state before you plan around it.

Flutter’s documentation is direct about the requirement: “Flutter apps built with Xcode 27 that do not adopt UIScene fail to launch on startup.” It is equally direct about the fix: as of Flutter 3.41, UIScene support is the default, and eligible apps are migrated automatically.

Check whether you were migrated

The migration runs when you build. Run flutter build ios or flutter run on Flutter 3.41 or later. Per the docs, a successful migration prints:

Finished migration to UIScene lifecycle

If the CLI can’t migrate your project, it warns you and gives manual instructions. The usual reason is an app delegate that has been customised — common in production apps that register method channels, set up push notifications or integrate native SDKs.

Manual migration for a customised app delegate

These steps are from Flutter’s official UIScene migration guide.

1. Register plugins in the new implicit-engine callback. Plugin registration moves out of didFinishLaunchingWithOptions:

@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
  }
}

2. Move method channels and platform views there too. Create them with the engine bridge’s messenger:

func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
  GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)

  let batteryChannel = FlutterMethodChannel(
    name: "samples.flutter.dev/battery",
    binaryMessenger: engineBridge.applicationRegistrar.messenger()
  )
}

The guide warns that reading the FlutterViewController from the window in didFinishLaunchingWithOptions “might crash” your app. That’s a common pattern in older channel setup code, so search for it.

3. Declare the scene in Info.plist, using Flutter’s scene delegate:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneClassName</key>
        <string>UIWindowScene</string>
        <key>UISceneDelegateClassName</key>
        <string>FlutterSceneDelegate</string>
        <key>UISceneConfigurationName</key>
        <string>flutter</string>
        <key>UISceneStoryboardFile</key>
        <string>Main</string>
      </dict>
    </array>
  </dict>
</dict>

4. Move UI life-cycle logic out of the app delegate. Once migrated, UIKit stops calling app-delegate methods about UI state, such as applicationDidBecomeActive. If you need a scene delegate of your own, Flutter requires you to subclass FlutterSceneDelegate or conform to FlutterSceneLifeCycleProvider:

import Flutter
import UIKit

class SceneDelegate: FlutterSceneDelegate {}

Then set UISceneDelegateClassName in Info.plist to your class instead of FlutterSceneDelegate.

Don’t forget your plugins

Your app is only as ready as its plugins. Flutter’s guide has a separate section for plugins that use iOS application life-cycle events. Upgrade plugins before the migration, not after, and treat any plugin that hooks app-delegate callbacks — notifications, deep links, attribution, background tasks — as something to test explicitly on an iOS 27 build.

Part 2: iPhone Duo layout

What reaches a Flutter app on iPhone Duo, and what does notWindow size and safe-area insets already flow from UIKit through the Flutter runtime to your code, so layout can adapt. The new iOS 27.1 APIs — reserved regions for the fold and cameras, the hinge, arrangements and vertical-bar priorities — have no Flutter binding. Reaching them requires a platform channel plugin (swift) that you build and maintain.already flows throughYour Dart codeFlutter runtimeMediaQuery, LayoutBuilderPlatform channel plugin (Swift)you build and maintain thisWindow size · safe-area insetsreported by UIKit on every foldreservedRegions · UIHingeInteractionarrangements · vertical-bar priorities (iOS 27.1)UIKit / SwiftUI on iOS 27.1
Your Flutter app can be correct on iPhone Duo — resizing and respecting insets — without any native code. Knowing where the fold is needs a bridge nobody ships today. Status checked on the date shown at the top of this post.

When iPhone Duo opens, closes or rotates, the Flutter view resizes. MediaQuery updates, dependent widgets rebuild, and LayoutBuilder re-runs with new constraints. A Flutter app built on constraints adapts.

The problems are code that assumed the screen can’t change shape while running.

Trap 1: a size captured once

// Breaks on iPhone Duo — captured once, stale after the device unfolds
class _HomeState extends State<Home> {
  late final double _width;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _width = MediaQuery.of(context).size.width; // throws on the second call, too
  }
}

Read the size where you build, and depend only on what you use:

@override
Widget build(BuildContext context) {
  final width = MediaQuery.sizeOf(context).width;
  return width >= 600 ? const TwoColumnHome() : const SingleColumnHome();
}

MediaQuery.sizeOf rebuilds the widget only when the size changes, not on every MediaQuery change. Inside a subtree, prefer LayoutBuilder, which answers the question you actually care about: how much space this widget has.

Trap 2: orientation locks and OrientationBuilder

Apple states that iPhone Duo’s inner display doesn’t honor your supported interface orientations. A Flutter app that calls SystemChrome.setPreferredOrientations to stay in portrait can’t rely on that on the inner display.

OrientationBuilder doesn’t help either. It reports orientation from the aspect ratio of the available space, and iPhone Duo’s inner display is close to square. Branch on width, not orientation: a single width breakpoint covers the outer display, the inner display in either orientation, and Split View.

Trap 3: symmetric padding

With toolbars along one side of the display, safe-area insets on iPhone Duo are often asymmetric. Apple’s guidance is to handle each side independently.

Why doubling one safe-area inset breaks layouts on iPhone DuoTwo panels of the inner display in landscape with a vertical bar on the leading edge. Left, labelled wrong: content width is calculated as the bounds minus twice the left inset, leaving a wasted strip on the right. Right, labelled correct: each side's inset is applied independently and the content fills the available space.✗ Assume both insets are equalwasted
let width = view.bounds.width
    - view.safeAreaInsets.left * 2
✓ Handle each side independently
let width = view.bounds
    .inset(by: view.safeAreaInsets).width
Apple’s own example. With bars on one side, the left and right insets are no longer equal — so any layout maths that doubles one side leaves dead space on the other, or clips content if it doubles the smaller one.Proportions come from Apple’s published pixel dimensions. Widths of insets, bars and the fold are illustrative — Apple has not published those values.
final padding = MediaQuery.paddingOf(context);

// Wrong on iPhone Duo — assumes left and right match
Padding(padding: EdgeInsets.symmetric(horizontal: padding.left), child: content);

// Right — or simply wrap in SafeArea, which applies each side separately
Padding(padding: EdgeInsets.only(left: padding.left, right: padding.right), child: content);

Trap 4: navigation bars Flutter draws itself

Where toolbars and tab bars go on iPhone DuoFive panels. A typical iPhone has bars at the top and bottom. iPhone Duo's outer display moves them to the side. The inner display in portrait keeps horizontal bars. The inner display in landscape puts them on the side. In Split View, each of the two apps places its bars along its own outer edge.TypicaliPhoneBars topand bottomDuo outerdisplayBars moveto the sideDuo inner,portraitKeepshorizontal barsDuo inner,landscapeBars onthe sideApp AApp BInner,Split ViewEach app’s barson its outer edge
The system does this automatically for standard bars — UINavigationController, UITabBarController, SwiftUI toolbars and TabView. Apple’s sample code notes that content from bars you place on screen yourself won’t be considered, so hand-built toolbars are where apps break.Proportions come from Apple’s published pixel dimensions. Widths of insets, bars and the fold are illustrative — Apple has not published those values.

On iPhone Duo the system moves its own tab bars and toolbars to the side of the outer display, and of the inner display in landscape.

This is our analysis. Flutter’s NavigationBar and BottomNavigationBar are drawn by Flutter, not UIKit, so iOS has no reason to move them. They will stay horizontal strips at the bottom of a wide display. If you want your app to match the platform, switch to a side layout yourself at wider widths — Material’s NavigationRail is the natural fit — and keep controls in the same relative positions across poses, as Apple’s guidelines ask.

Trap 5: full-screen 16:9 video

16:9 video on a typical iPhone versus iPhone Duo's inner displayDrawn to scale from Apple's figures. In landscape, 16:9 video leaves 18% of a typical iPhone unused and 20% of the Duo inner display unused, but the Duo picture is 6.2″ wide against 4.7″. In portrait on the inner display, full-width 16:9 video leaves 60% of the screen unused.16:9Typical iPhone, landscape18% unused · picture 4.7″ wide16:9Duo inner, landscape20% unused · picture 6.2″ wide16:9Duo inner, portrait60% unused · picture 4.4″ wide
In landscape the unused area is similar — 18% versus 20% — the bars just move to the top and bottom, and the picture is physically larger. Portrait is the real problem: full-width 16:9 leaves 60% of the inner display empty. That is where a player above a list, rather than a full-screen player, earns its place.

The numbers are more nuanced than “foldables mean black bars.” In landscape, 16:9 video wastes a similar share of iPhone Duo’s inner display as a normal iPhone — the bars just move to the top and bottom — and the picture is physically bigger. Portrait is where full-width 16:9 wastes most of the screen. There, a player in an AspectRatio(aspectRatio: 16 / 9) above a scrolling list uses the space well.

Part 3: the fold, and Flutter’s empty displayFeatures

Flutter has had foldable support since 2021: MediaQuery.displayFeatures describes hinges, folds and cutouts, and DisplayFeatureSubScreen keeps dialogs and sheets off them. On Android foldables it works.

On iOS it is empty. Flutter’s API documentation says so plainly: DisplayFeature “is populated only on Android.”

Flutter's foldable support works on Android and is empty on iPhone DuoTop, Android: fold information flows from the Android fold APIs through Flutter's Android embedder into MediaQuery.displayFeatures, and DisplayFeatureSubScreen keeps dialogs and sheets off the fold. Bottom, iPhone Duo: iOS 27.1 reports the fold as a reserved region, but Flutter's iOS embedder does not read it, so displayFeatures is always empty and dialogs can sit across the fold.Android fold APIshinge position and stateAndroid embedderreads the folddisplayFeatures[ fold ]DisplayFeatureSubScreendialogs avoid the foldUIViewReservedRegioniOS 27.1 · .divisioniOS embeddernot wired to regionsdisplayFeatures[ ] — always emptyDisplayFeatureSubScreendialogs sit on the foldAndroid foldablesiPhone Duo
Flutter’s API documentation says DisplayFeature “is populated only on Android.” A community proposal to change that for iPhone Duo is open. Until something ships, your Flutter app resizes correctly but cannot tell where the fold is — including inside Flutter’s own dialog positioning.

A community proposal to populate it for iPhone Duo, flutter#192515, is open and labelled P2 and team-ios, with no response from the team so far. Separately, flutter#192667 reports that inside a dialog confined to one half of a foldable, MediaQuery.sizeOf still reports the whole screen. That matters the moment display features are populated on any platform.

So today, a Flutter app on iPhone Duo resizes correctly but can’t tell where the fold is — including inside Flutter’s own dialog positioning.

A plugin that fills the gap without inventing an API

Most apps don’t need the fold. If yours does — split video calls, reading apps, games — the lowest-regret design is not a new API. Feed iOS 27.1’s reserved regions into the DisplayFeature model Flutter already has. Then DisplayFeatureSubScreen, and any code written for Android foldables, works unchanged. When Flutter populates it natively, you delete the plugin.

A proposed Flutter plugin that feeds iPhone Duo's fold into DisplayFeatureA Swift plugin observes iOS 27.1 reserved regions and sends their frames over an EventChannel. Dart code converts them into DisplayFeature objects of type fold and overrides MediaQuery's displayFeatures, so Flutter's own DisplayFeatureSubScreen and any code written for Android foldables start working on iPhone Duo.iOS 27.1Plugin — proposedDartreservedRegions(kind:).division, includeInactiveDuoFoldPlugin.swiftobserves geometry changesEventChannel"duo/fold"DisplayFeature(type: .fold)built from region framesMediaQuery overridecopyWith(displayFeatures:)SDK widgets + your codedialogs avoid the fold
A design, not a shipped plugin. Routing through DisplayFeature rather than inventing a new API means nothing above it changes — and when Flutter populates it natively, you delete the plugin. The SDK the Swift side needs arrived in the Xcode 27.1 beta on 18 September 2026, but we have not compiled this design against it.

The Dart side uses only stable Flutter APIs. DisplayFeature has a public constructor, MediaQueryData.copyWith accepts displayFeatures, and DisplayFeatureType.fold is documented as a fold in a flexible screen without a physical gap:

import 'dart:ui' show DisplayFeature, DisplayFeatureState, DisplayFeatureType;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

/// Merges iPhone Duo's fold into MediaQuery.displayFeatures for its subtree.
class DuoFoldScope extends StatefulWidget {
  const DuoFoldScope({super.key, required this.child});
  final Widget child;

  @override
  State<DuoFoldScope> createState() => _DuoFoldScopeState();
}

class _DuoFoldScopeState extends State<DuoFoldScope> {
  static const _channel = EventChannel('duo/fold');
  late final Stream<List<DisplayFeature>> _folds =
      _channel.receiveBroadcastStream().map(_toFeatures);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<DisplayFeature>>(
      stream: _folds,
      initialData: const [],
      builder: (context, snapshot) {
        final folds = snapshot.data ?? const [];
        if (folds.isEmpty) return widget.child;
        final data = MediaQuery.of(context);
        return MediaQuery(
          data: data.copyWith(displayFeatures: [...data.displayFeatures, ...folds]),
          child: widget.child,
        );
      },
    );
  }

  static List<DisplayFeature> _toFeatures(dynamic event) => [
        for (final f in (event as List).cast<Map>())
          DisplayFeature(
            bounds: Rect.fromLTWH(
              (f['x'] as num).toDouble(),
              (f['y'] as num).toDouble(),
              (f['width'] as num).toDouble(),
              (f['height'] as num).toDouble(),
            ),
            type: DisplayFeatureType.fold,
            state: DisplayFeatureState.postureHalfOpened,
          ),
      ];
}

The native side reads the fold with the API Apple showed in its “Strike a pose” Tech Talk and streams the frames. iOS points and Flutter logical pixels are the same unit, so the frames can be used directly:

// NOT COMPILED: requires the iOS 27.1 SDK in Xcode 27.1, which is not yet
// released. API names are from Apple's Tech Talk sample code.
final class DuoFoldStreamHandler: NSObject, FlutterStreamHandler {
  private var sink: FlutterEventSink?

  func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
    sink = events
    return nil
  }

  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    sink = nil
    return nil
  }

  /// Call when the Flutter view lays out.
  func publishFolds(for flutterView: UIView) {
    let frames = flutterView.reservedRegions(kind: .division).map(\.frame)
    sink?(frames.map { ["x": $0.minX, "y": $0.minY, "width": $0.width, "height": $0.height] })
  }
}

Register its FlutterEventChannel in didInitializeImplicitFlutterEngine using engineBridge.applicationRegistrar.messenger(), as in Part 1.

Three design notes:

  • An empty list is the normal case. Apple says the fold’s region is only active when the device is folded, and has zero width when flat. Since only active regions are returned by default, “no features” means “no fold right now.”
  • Use the hinge for effects, not layout. Apple’s scenes Tech Talk: hinge data “is ideal for driving interactions or effects. For layout, use the arrangement and region APIs.”
  • Don’t ship it untested. You can now test it: the iPhone Duo simulator arrived in the Xcode 27.1 beta on 18 September 2026. Note that the simulator cannot run most app extensions and has no StandBy, so those still need hardware — see what the Xcode 27.1 beta can and cannot do.

A plan, in order

  1. Now: move to Flutter 3.41 or later, upgrade plugins, and build for iOS. Confirm the scene migration ran, or do it manually. Retest notifications, deep links and every plugin that touches app life-cycle events.
  2. Now: search for sizes captured in initState or didChangeDependencies, EdgeInsets.symmetric built from one padding side, orientation locks and OrientationBuilder. Replace them with MediaQuery.sizeOf, LayoutBuilder, per-side padding and width breakpoints.
  3. Now: decide how navigation should look at wide widths.
  4. Now, in the Xcode 27.1 beta: run the app on the iPhone Duo simulator in Device Hub, through every pose and Split View. The first boot takes several minutes; after that it behaves.
  5. Only if your product needs it: build the fold plugin, and watch flutter#192515 so you can delete it later.

Our rough planning estimate for an established app, not a quote: for a Flutter app with an unmodified app delegate, step 1 can be close to free; with a heavily customised one, budget days. Step 2 is typically days. A navigation redesign or the fold plugin is separate work worth scoping on its own.

For a per-issue reference of what breaks on iPhone Duo, see the Flutter guide on iphoneduosupport.com. For every other change in the iOS 27 SDK, see iOS 27 for developers, and for the toolchain itself, the Xcode 27.1 beta and its iPhone Duo simulator.

Apple sources

Checked against these sources on . Apple’s written “Preparing your app for iPhone Duo” documentation and Xcode 27.1 beta were still listed as coming later this month on that date, so the Tech Talks and the Human Interface Guidelines are the authoritative references. We will update this post when the written documentation ships.

This post is part of our work onMobile app development for iOS and Android

Related reading

Let's talk

Find out what iPhone Duo and iOS 27 mean for your app

Send us your App Store link and the stack it is built on. An engineer replies with what breaks, what to fix first, and a realistic effort estimate — no call and no obligation.

  • A written reply from an engineer
  • No call required
  • No obligation