Integrating Swift Foundation Models in Flutter Apps with Pigeon

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
FoundationModelsframeworkPigeon (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()andpredict(prompt).Swift implementation talks to
FoundationModelsand 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.availabilityand branch. If available, we proceed; otherwise we fail fast with a descriptivePigeonError(e.g.,device_not_eligible,apple_intelligence_disabled,model_not_ready, or a genericmodel_unavailable).Session setup: On success, we create a single
LanguageModelSessionwith a system-level instruction string and store it in a private field. This keeps conversational context consistent across multiplepredictcalls.Predict flow:
predict(prompt:)validates that the session exists (you calledinitialize()first) and that the prompt isn’t empty. Then it awaitssession.respond(to:)and returnsresponse.contentback 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 aBasicMessageChannelback to Flutter.Error mapping: Any thrown Swift errors or our explicit
PigeonErrors becomePlatformExceptions 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 inpredict.Swift Concurrency:
Task+awaitto 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
FoundationModelsships.The
FoundationModelsframework 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, andmodel_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
FoundationModelsmissing: 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 💙💙💙



