Flitzdocs
Loader plugins

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.

ios/Podfile
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>'
end

The first time on a machine, register the spec repo, then install:

pod repo add flitz <cocoapods-spec-repo-url>
pod install

pod 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.

ios/Runner/AppDelegate.swift
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:

ios/Runner/SceneDelegate.swift
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:

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 publish prints (or open its flitz://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 iOS path is 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:

ios/Runner/AppDelegate.swift
#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
}
ios/Runner/SceneDelegate.swift
#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.

ios/Podfile
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.

ios/Runner/SceneDelegate.swift
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.

Next steps

On this page