iOS
Add the loader to an iOS host with CocoaPods or Swift Package Manager.
The iOS loader ships as FlitzLoader.xcframework — a device-only (arm64)
binary for iOS 16.0 and later, exposed as the Swift module FlitzLoader. It is
published both as a CocoaPods pod and as a Swift package; both resolve the same
binary, and import FlitzLoader works the same way.
Before you start, make sure your app builds against the pinned Flitz SDK and
that flitz plugins credentials --write has stored the repository token on
this machine (see Credentials). In the snippets below,
<plugin-version> is the value flitz plugins configure prints for your SDK
pin.
Set the deployment target to iOS 16.0
FlitzLoader requires iOS 16.0. The Flutter template targets an older version,
and pod install or package resolution fails until you raise it in two
places:
- In
ios/Podfile, set the platform line (the template ships it commented out):platform :ios, '16.0'. - In Xcode, set the
Runnertarget's Minimum Deployments to 16.0 (IPHONEOS_DEPLOYMENT_TARGET = 16.0).
Add the dependency
Add the private spec source and the versioned pod to ios/Podfile, next to
the standard flutter_install_all_ios_pods line. Keep the CDN source: declaring
any explicit source disables the implicit one. Authentication comes from
~/.netrc.
platform :ios, '16.0'
source 'https://cdn.cocoapods.org/'
source 'https://dl.cloudsmith.io/basic/leancode/flitz/cocoapods/index.git'
target 'Runner' do
use_frameworks! :linkage => :static
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
pod 'FlitzLoader', '<plugin-version>'
endThe first time on a machine, register the spec repo, then install:
pod repo add flitz https://dl.cloudsmith.io/basic/leancode/flitz/cocoapods/index.git
pod installThe pod adds one build phase, Flitz SDK floor check, that runs before every
compile. It fails the build when the Flutter SDK named in
ios/Flutter/Generated.xcconfig is not a Flitz SDK, or is older than this
plugin version supports; the error names the fix. There is nothing else to
enable.
Subclass the app delegate
In ios/Runner/AppDelegate.swift, extend BundleLoaderAppDelegate instead of
FlutterAppDelegate, and register your plugins through its two hooks. Merge
these lines into your existing delegate; do not replace the file.
import UIKit
import Flutter
import FlitzLoader
@main
@objc class AppDelegate: BundleLoaderAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// … your existing launch setup …
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Your app's own plugins, on the engine that renders your UI.
override func registerHostPlugins(with engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
// The same plugins on every bundle engine.
override func registerPlugins(with engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
}BundleLoaderAppDelegate already implements FlutterImplicitEngineDelegate,
and its didInitializeImplicitFlutterEngine(_:) cannot be overridden. Remove
the template's FlutterImplicitEngineDelegate conformance and that method, and
move your plugin registration into registerHostPlugins(with:). On an older
template that calls GeneratedPluginRegistrant.register(with: self) in
didFinishLaunchingWithOptions, move that registration there too.
registerPlugins(with:) runs once per bundle engine, right after that engine
starts and before it is shown. The loader cannot call GeneratedPluginRegistrant
itself — that class is generated in your project — so without the override,
bundles run with no plugins and method-channel calls fail with
MissingPluginException.
Change the scene delegate's base class
A current flutter create project has ios/Runner/SceneDelegate.swift
(class SceneDelegate: FlutterSceneDelegate {}) and an Info.plist whose
UISceneDelegateClassName already names $(PRODUCT_MODULE_NAME).SceneDelegate.
Change the one base class:
import Flutter
import UIKit
import FlitzLoader
class SceneDelegate: BundleLoaderSceneDelegate {}This step is required in a UIScene app: incoming URLs and the launch
lifecycle are delivered to the scene delegate, not the app delegate.
BundleLoaderSceneDelegate routes flitz:// URLs, forwards the bundle's own
deeplinks to the running bundle engine, and replays the scene connection into
that engine — which is created late, after the bundle downloads. Without the
replay, a bundle cold-launched from a flitz://download link never delivers
its launch event to scene-aware plugins, and plugins such as
FirebaseMessaging hang on init. The scene delegate also opens the panel for
flitz-panel links, and with Launch set to panel it puts the panel on
screen at a cold start — see
Opening the panel.
No SceneDelegate.swift?
If your project predates the UIScene template, either add the file above and
set UISceneDelegateClassName to $(PRODUCT_MODULE_NAME).SceneDelegate, or
point that key straight at FlitzLoader.BundleLoaderSceneDelegate with no
Swift file. iOS only uses the class named in Info.plist.
Register the URL scheme
iOS does not auto-merge plist contributions. Register the URL scheme in
ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>FlitzLoader</string>
<key>CFBundleURLSchemes</key>
<array>
<string>flitz</string>
</array>
</dict>
</array>Keep the entry's name FlitzLoader. Without
sign-in the loader ignores the name; with sign-in
on, the name is how the loader finds its scheme, and with any other name every
load fails with "Loader build is broken".
To use your own scheme, change the single <string>flitz</string> value — the
loader recognizes its URLs by authority, not scheme. A scheme starts with a
letter and uses lowercase letters, digits, +, -, or ., at most 32
characters — the only form flitz publish accepts. With sign-in on, keep the
scheme your client id is registered for. See
Custom schemes.
Build and run
Build in release mode — a debug host cannot run bundles — and install on a physical device; the loader framework has no Simulator slice, so a Simulator build does not link:
flutter build ios --release
xcrun devicectl device install app --device <device-id> build/ios/iphoneos/Runner.appxcrun devicectl list devices prints the device identifier. Avoid
flutter build ipa here: its default export is for App Store upload, and the
result does not install on a development device.
Then open a bundle: open the landing page flitz publish prints on the device
and tap its button, or open its flitz://download?url=https://… link directly.
The loader downloads the bundle over HTTPS and runs it. With
sign-in on, the loader signs you in before it
downloads.
A cold launch from the link (app not already running) exercises the scene-connection replay from the scene-delegate step — the path scene-aware plugins depend on. To test it, launch the app from the link:
xcrun devicectl device process launch --device <device-id> --terminate-existing \
--payload-url 'flitz://download?url=<percent-encoded-bundle-url>' <bundle-identifier>Loading a bundle takes over the app
On iOS, asking for a bundle ends your app's UI for the rest of the session, and
so does opening the panel. The moment a download link arrives — even a
malformed one — the loader becomes the window's root view controller and shuts
down the Flutter engine that renders your UI. The sign-in screen, the download
screen, errors, the panel, and the bundle itself all run as the window root;
your own UI comes back only when the app is relaunched. The next bundle
replaces the current one the same way.
A build started from the panel can go back to the panel until the bundle draws its first frame — see Leaving the panel.
Plan your app around it:
- Nothing in your app's Dart code survives a load. State, timers, open
channels, and in-flight work go with the engine. Persisted data — files,
UserDefaults, databases, the keychain — stays, and the bundle can read and change it. - Native, process-level state survives, because the process does: the app
delegate, a configured
FirebaseApp, an open socket. - Initialize process-wide native SDKs once. If your app configures
Firebase natively (a
GoogleService-Info.plistin theRunnertarget, orFirebaseApp.configure()), the bundle callsFirebase.initializeApp()without options; passing options fails with[core/duplicate-app]. If your app does not, the bundle callsFirebase.initializeApp(options: …)itself.
One target, with and without the loader
If the same target is both your normal app and a loader build, do not invent a
build flag. Gate every loader reference on whether the module is linked, using
Swift's canImport:
import UIKit
import Flutter
#if canImport(FlitzLoader)
import FlitzLoader
typealias AppDelegateBase = BundleLoaderAppDelegate
#else
typealias AppDelegateBase = FlutterAppDelegate
#endif
@main
@objc class AppDelegate: AppDelegateBase {
// … shared launch setup …
#if canImport(FlitzLoader)
override func registerHostPlugins(with engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
override func registerPlugins(with engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
#endif
}
// Without the loader, your plugins register through the template's own hook.
#if !canImport(FlitzLoader)
extension AppDelegate: FlutterImplicitEngineDelegate {
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
#endifimport Flutter
import UIKit
#if canImport(FlitzLoader)
import FlitzLoader
typealias SceneDelegateBase = BundleLoaderSceneDelegate
#else
typealias SceneDelegateBase = FlutterSceneDelegate
#endif
class SceneDelegate: SceneDelegateBase {}The only knob is whether the dependency is linked:
Gate the pod on an environment variable — the iOS counterpart to Android's
enabledFlavors. The Podfile reads it whenever pod install runs, and
flutter build ios and flutter run can run pod install for you, so set the
variable for those commands too:
pod 'FlitzLoader', '<plugin-version>' if ENV['FLITZ_ENABLED'] == 'true'FLITZ_ENABLED=true pod install --project-directory=ios
FLITZ_ENABLED=true flutter build ios --releaseNext steps
Sign-in and the panel
Enterprise, optional: turn on sign-in and the panel with a FlitzLoader dictionary in Info.plist.
URL scheme
What flitz://download accepts, and how to use your own scheme.
Compatibility
What must match between a host and a bundle, and how to fix a mismatch.
Troubleshooting
Load-time rejections and integration mistakes.