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, 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 Runner target'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.

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

The 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 install

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

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

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

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

xcrun 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.plist in the Runner target, or FirebaseApp.configure()), the bundle calls Firebase.initializeApp() without options; passing options fails with [core/duplicate-app]. If your app does not, the bundle calls Firebase.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:

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

ios/Podfile
pod 'FlitzLoader', '<plugin-version>' if ENV['FLITZ_ENABLED'] == 'true'
FLITZ_ENABLED=true pod install --project-directory=ios
FLITZ_ENABLED=true flutter build ios --release

Next steps

On this page