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, so pick whichever your project already uses 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 in
~/.netrc on this machine (see the overview).
In the snippets below, <cocoapods-spec-repo-url>, <swift-repo-url>, and
<plugin-version> are the values flitz plugins configure prints for your
SDK pin.
Let an agent apply the wiring
flitz plugins skill prints these same additions with the concrete
repository URLs and plugin version already substituted, as a prompt you can
hand to a coding agent. The steps below explain what that wiring does.
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.
source 'https://cdn.cocoapods.org/'
source '<cocoapods-spec-repo-url>'
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 <cocoapods-spec-repo-url>
pod installpod install adds two script phases that run before every compile and write
flitz_loader_available_plugins.json (the plugins your pubspec.yaml
declares) and flitz_loader_host_config.json (the expected
platform_dill_hash values derived from your FLUTTER_ROOT) into a
FlitzLoaderAvailablePlugins.bundle resource inside Runner.app. Nothing to
enable by hand.
Subclass the app delegate
In ios/Runner/AppDelegate.swift, extend BundleLoaderAppDelegate instead of
FlutterAppDelegate and override registerPlugins(with:) to register your
plugins on each bundle engine. Merge these lines into your existing
delegate; do not replace the file.
import UIKit
import Flutter
import FlitzLoader
@main
@objc class AppDelegate: BundleLoaderAppDelegate, FlutterImplicitEngineDelegate {
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 implicit engine — as generated by the template.
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
// Loader: register the same plugins on every bundle engine.
override func registerPlugins(with engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
}BundleLoaderAppDelegate routes flitz:// URLs that arrive through
application(_:open:options:) and presents the loader UI over your root view
controller. registerPlugins(with:) is called once per bundle engine, before
the engine runs. The loader cannot call GeneratedPluginRegistrant itself —
that class is generated in your project — so if you omit the override, bundles
run with no plugins and method-channel calls fail with MissingPluginException.
If your project predates the UIScene template, keep the
GeneratedPluginRegistrant.register(with: self) call the template put in
didFinishLaunchingWithOptions and add only the registerPlugins(with:)
override.
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.
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.
Edit Info.plist
iOS does not auto-merge plist contributions. Register the URL scheme and the
camera usage string in ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>flitz URL scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<string>flitz</string>
</array>
</dict>
</array>
<key>NSCameraUsageDescription</key>
<string>Camera access is needed to scan QR codes containing bundle URLs.</string>To use your own scheme, change the single <string>flitz</string> value — the
loader recognizes its URLs by authority, not scheme. See
Custom schemes.
Build and run
Build in profile or release mode and install on a physical device — the engine ships no simulator slice, so a Simulator build does not link and a debug host rejects bundles. Then open a bundle:
- Scan the QR code
flitz publishprints (or open itsflitz://download?url=https://…link) to download and run a bundle over HTTPS. - Or open
flitz://run?path=<filename>for a bundle already placed in the app's Documents directory. On iOSpathis a bare filename inside Documents, not an absolute path.
A cold launch from the link (app not already running) exercises the scene-connection replay from the previous step — the path scene-aware plugins depend on.
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:
#if canImport(FlitzLoader)
import FlitzLoader
typealias AppDelegateBase = BundleLoaderAppDelegate
#else
typealias AppDelegateBase = FlutterAppDelegate
#endif
@main
@objc class AppDelegate: AppDelegateBase {
// … shared launch setup, FlutterImplicitEngineDelegate, etc. …
#if canImport(FlitzLoader)
override func registerPlugins(with engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
#endif
}#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. Re-run pod install after changing it; the Podfile reads it
at install time.
pod 'FlitzLoader', '<plugin-version>' if ENV['FLITZ_ENABLED'] == 'true'Optional: make the scanner the first screen
To open on the QR scanner instead of your own UI, install
BundleLoader.scannerViewController() as the window root in
scene(_:willConnectTo:options:). Always call super first — the base class
captures the scene connection it later replays into the bundle engine.
import UIKit
import FlitzLoader
class SceneDelegate: BundleLoaderSceneDelegate {
override func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
super.scene(scene, willConnectTo: session, options: connectionOptions)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = BundleLoader.scannerViewController()
window.makeKeyAndVisible()
self.window = window
}
}
}Do not subclass FlutterViewController to host scanner UI; the factory above
is the only supported entry point for this mode.