Skip to main content

Command Palette

Search for a command to run...

Integrating Swift Foundation Models in Flutter Apps with Pigeon

Published
5 min read
Integrating Swift Foundation Models in Flutter Apps with Pigeon
P

Hey there! I'm Pranav Masekar, a dedicated Flutter developer who's all about crafting captivating mobile apps that not only look stunning but also deliver unforgettable user experiences. But it doesn't stop there – I'm also a passionate blogger, sharing insights and best practices within the Flutter community. By contributing to the growth and knowledge-sharing of fellow developers, I'm committed to fostering a dynamic and collaborative environment. And let's not forget my DevOps journey – as a seasoned engineer, I've got the CI/CD pipelines, infrastructure-as-code, and cloud platforms down to an art.

Introduction

If you're a Flutter dev on iOS, this one's exciting! Apple shipped Foundation Models APIs in the latest SDKs, and you can call them from Flutter with a lightweight Swift bridge. In this post, we'll integrate Swift Foundation Models into a Flutter app using Pigeon, wire up a simple chat-like UI, and return AI responses—all locally on-device when available. Trust me, this is way simpler than you think.

  • Repo: [Link]

  • Target: iOS (Objective-C/Swift host) via Flutter

Prerequisites

  • Xcode 16+ (Xcode 16 SDKs that include Apple Intelligence Foundation Models)

  • iOS 18 simulator or device with Apple Intelligence support

  • Flutter 3.24+

  • Swift toolchain with access to the FoundationModels framework

  • Pigeon (codegen for platform channels)

Pro tip: Foundation Models availability is device/region/entitlement dependent. The code handles unavailability gracefully.

Architecture Overview

  • Pigeon host API defines initialize() and predict(prompt).

  • Swift implementation talks to FoundationModels and exposes results back to Dart.

  • AppDelegate wires the Swift implementation to the Flutter binary messenger.

  • Flutter UI calls the host API and renders responses.

Define the Pigeon API (Dart)

We declare a host-side API that Swift implements.

import 'package:pigeon/pigeon.dart';

@ConfigurePigeon(
  PigeonOptions(
    dartOut: 'lib/foundation_models_api.dart',
    dartOptions: DartOptions(),
    kotlinOut:
        'android/app/src/main/kotlin/com/example/ios_playground/FoundationModelsApi.kt',
    kotlinOptions: KotlinOptions(),
    swiftOut: 'ios/Runner/FoundationModelsApi.swift',
    swiftOptions: SwiftOptions(),
    dartPackageName: 'com.pranav.iosPlayground',
  ),
)
@HostApi()
abstract class FoundationModelsApi {
  @async
  void initialize();

  @async
  String predict(String prompt);
}

Then run Pigeon to generate bindings (already generated in repo):

flutter pub run pigeon --input pigeons/foundation_models_api.dart

Implement the Swift bridge using Foundation Models

Here's where the magic happens, Swift uses FoundationModels and implements Pigeon protocol.

class FoundationModelsImplementation: FoundationModelsApi {
    private var session: LanguageModelSession?

    func initialize(completion: @escaping (Result<Void, Error>) -> Void) {
        Task {
            do {
                // Check if Foundation Models is available
                let model = SystemLanguageModel.default

                switch model.availability {
                case .available:
                    // Create a session with simple instructions
                    print("Model is available")
                    let instructions = """
                        You are a helpful AI assistant. Provide clear, concise, and helpful responses to user questions.
                        """
                    self.session = LanguageModelSession(instructions: instructions)
                    completion(.success(()))

                case .unavailable(.deviceNotEligible):
                    // ... other unavailability cases returning PigeonError
                    completion(
                        .failure(
                            PigeonError(
                                code: "device_not_eligible",
                                message: "Foundation Models is not available on this device.",
                                details: nil
                            )
                        )
                    )
                // ... more cases ...
                }
            } catch {
                completion(.failure(error))
            }
        }
    }

    func predict(prompt: String, completion: @escaping (Result<String, Error>) -> Void) {
        Task {
            guard let session = session else {
                completion(.failure(PigeonError(code: "not_initialized", message: "Call initialize() before predict().", details: nil)))
                return
            }
            guard !prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
                completion(.failure(PigeonError(code: "invalid_argument", message: "Prompt must not be empty.", details: nil)))
                return
            }
            do {
                let response = try await session.respond(to: prompt)
                completion(.success(response.content))
            } catch {
                completion(.failure(error))
            }
        }
    }
}

How the implementation works (step-by-step):

  • Availability gate: We read SystemLanguageModel.default.availability and branch. If available, we proceed; otherwise we fail fast with a descriptive PigeonError (e.g., device_not_eligible, apple_intelligence_disabled, model_not_ready, or a generic model_unavailable).

  • Session setup: On success, we create a single LanguageModelSession with a system-level instruction string and store it in a private field. This keeps conversational context consistent across multiple predict calls.

  • Predict flow: predict(prompt:) validates that the session exists (you called initialize() first) and that the prompt isn’t empty. Then it awaits session.respond(to:) and returns response.content back to Dart.

  • Async + safety: Both methods run inside Task { ... } to avoid blocking the main thread. Results are returned via the completion handler, which Pigeon forwards over a BasicMessageChannel back to Flutter.

  • Error mapping: Any thrown Swift errors or our explicit PigeonErrors become PlatformExceptions in Dart, so the Flutter side can show friendly error messages.

Why this is nice:

  • Availability handling: clean branching on SystemLanguageModel.default.availability.

  • Session lifecycle: create once on initialize, reuse in predict.

  • Swift Concurrency: Task + await to call the model.

Wire the bridge in AppDelegate

We register our Swift implementation with Flutter so messages flow.

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    let foundationModelsImplementation = FoundationModelsImplementation()

    let controller = window?.rootViewController as! FlutterViewController

    FoundationModelsApiSetup.setUp(
          binaryMessenger: controller.binaryMessenger,
          api: foundationModelsImplementation
      )

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Flutter UI: initialize and call predict()

Simple UI that calls initialize() once and then predict() on button press.

class _HomeViewState extends State<HomeView> {
  final FoundationModelsApi _foundationModelsApi = FoundationModelsApi();
  // ...
  Future<void> _initializeModel() async {
    setState(() { _isInitializing = true; });
    try {
      await _foundationModelsApi.initialize();
      setState(() { _isInitialized = true; _isInitializing = false; });
    } catch (e) { setState(() { _isInitializing = false; }); }
  }

  Future<void> _generateResponse() async {
    if (_textController.text.trim().isEmpty) return;
    setState(() { _isGenerating = true; });
    try {
      final response = await _foundationModelsApi.predict(
        _textController.text.trim(),
      );
      setState(() { _response = response; _isGenerating = false; });
    } catch (e) { setState(() { _isGenerating = false; }); }
  }
}

Notes on Xcode/SDK Requirements

  • You need Xcode 16+ with iOS 18 SDK where FoundationModels ships.

  • The FoundationModels framework must be available at compile time.

  • Runtime availability depends on device/region and Apple Intelligence settings.

  • The sample handles errors like device_not_eligible, apple_intelligence_disabled, and model_not_ready.

Run it

flutter run -d ios

If the model is available, you'll see successful initialization and responses to prompts.

Screenshot

Single screenshot of the app showing initialisation and a generated response.

Troubleshooting

  • Build errors about FoundationModels missing: ensure Xcode is updated and using the latest iOS SDK.

  • Initialization failures: check device eligibility and Apple Intelligence settings (iOS 18).

  • Empty prompt errors: UI prevents it, but the Swift side validates too.

Wrap-up

So there you have it! We defined a tiny Pigeon API, implemented it in Swift using Apple’s Foundation Models, wired it through AppDelegate, and built a minimal Flutter UI to ask questions and render answers. Obviously, you can extend this to streaming tokens, system prompts per-session, or function-calling.

Keep Fluttering 💙💙💙