How to add Flutter to an existing iOS app
Adding a Flutter screen to an existing iOS app looks just as easy in the official documentation as it does on Android: you create a Flutter module, link it into the Xcode project, and present a FlutterViewController from Swift. For a fresh demo app, that's really all there is to it. In a real iOS app with years of history, that first screen is again the easy part - and the places where it gets harder are different from Android.
What add-to-app gives an iOS team is the same: screens written once for both platforms, and a Flutter module your developers can iterate on with hot reload while the native app keeps shipping. What it asks of the host is iOS-specific. Your custom build configurations decide whether Flutter ships debug or release code. Flutter screens have to behave inside a UINavigationController the way iOS users expect. This page goes through those decisions from the point of view of the iOS tech lead who has to make them, together with the problems we have seen along the way.
As on Android, most of the plumbing involved is generic add-to-app work that every team ends up building in some form. We built Inlay to cover it and point to it where it's relevant, but the decisions are the same whether you use a framework or write it yourself. For the step-by-step setup, see the iOS section of Inlay's Getting started; for which screens to migrate first, see the planning page and, from your own code, Converge.
What changes in your iOS project
Flutter arrives as a module with a hidden . wrapper project; the host links it as a dependency and nothing in the host moves. As on Android, Flutter code lives in exactly one module per app, so plan for a shared module split into packages (Scaling & code sharing), and keep the module runnable as a standalone Flutter app so that day-to-day work doesn't go through Xcode. Native Swift code that belongs to the Flutter side goes into a plugin package the module depends on, not into the wrapper project, which is regenerated by the tooling.
Integrating Flutter with Swift Package Manager, CocoaPods or prebuilt XCFrameworks
There are multiple ways to embed Flutter binaries within your iOS app:
- Swift Package Manager is the current recommended path (Flutter 3.44+): the module produces a local package the host adds to its project, with a build phase that rebuilds Dart when it changes. New integrations should go here.
- CocoaPods still works, but Flutter keeps it in maintenance mode, and the CocoaPods registry becomes read-only on 2 December 2026. If your host is on CocoaPods today, the Flutter integration is a reason to plan the move, not to postpone it.
- Prebuilt XCFrameworks (
flutter build ios-framework) keep the Flutter SDK out of the host's build entirely: the host linksDebug,ProfileandReleaseframeworks like any vendor SDK. This is the right choice when a separate team owns the module or when the host's CI can't install Flutter - at the cost of a publishing pipeline and a new artifact for every Dart change.
Build configurations
Custom Xcode configurations can require extra setup when embedding Flutter on iOS. Flutter’s build tooling uses FLUTTER_BUILD_MODE if set; otherwise it infers Debug, Profile, or Release from the configuration name. Names such as Staging, UAT, and AppStore therefore need an explicit Flutter build mode. When integrating prebuilt XCFrameworks, map each host configuration to the intended Flutter artifacts and keep the engine and Dart app variants consistent.
Navigation that behaves like iOS
When mixing Flutter screens with native screens, it is important that users get a seamless experience and cannot notice which screens are which. Few navigation concepts need to be taken into account:
- The swipe-back gesture. When a Flutter screen has inner routes, the
UINavigationControlleredge swipe would pop the whole Flutter view controller - every inner route at once. The gesture has to be disabled while Flutter has routes to pop and re-enabled at its root. Inlay'sInlayNativePopGestureObserverkeeps the two stacks in sync. - The navigation bar. If Flutter draws its own
AppBar, hide the native bar while the Flutter screen is on top; if you keep the native bar, Flutter has to tell it the title and back state. Decide once for the whole app. - Custom transitions. Native navigation bars with custom animations don't always play well with Flutter. In one of our projects, it quickly turned out to be faster to rewrite a few more screens in Flutter than to make the animations work together.
Before the first Flutter screen goes to production
- Local network permission. Hot reload and DevTools need local network access on iOS 14+. The setup guide puts it in a debug-only plist and warns never to ship it in
Release, because the App Store might reject the app. With custom configurations, make sureStagingorUATdoesn't carry it into a release build. - Deployment target. At least what Flutter, your plugins and Inlay require. Flutter raises it from time to time (3.47 moved to iOS 15), which can force a product decision about old devices.
- An archived build on a device. Simulator and debug builds hide problems with build modes, signing of embedded frameworks and plugin registration.
- Status bar and dark mode. Check Flutter screens in both modes and whether the status bar style follows the Flutter screen - hosts that manage it globally can override it.
- Crash reporting. Dart errors need explicit reporting and symbol upload.