feat(apple-ai): support Apple's Foundation Models framework with local-first and cloud Gemini fallback#1855
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces integration with Apple's Foundation Models (Apple Intelligence) in the Firebase AI Example app, adding a new "Apple Intelligence" feature screen, a custom local places tool with Google Maps grounding, and a view model to manage hybrid summarization, planning, and visual identification. The review feedback highlights several critical improvements: correcting the iOS availability annotations and checks from future versions (iOS 26.0/27.0) to iOS 18.2, using array indices instead of optional properties as identifiers in SwiftUI ForEach loops to prevent rendering bugs, and refactoring the custom tool into a Sendable struct to ensure concurrency safety.
| import FoundationModels | ||
| import FirebaseAILogic | ||
|
|
||
| @available(iOS 27.0, *) |
|
|
||
| // Day Plans | ||
| if let days = itinerary.days { | ||
| ForEach(days, id: \.title) { day in |
There was a problem hiding this comment.
Using an optional property like title (which is String? in DayPlan.PartiallyGenerated) as the id for ForEach is unsafe. During streaming or partial generation, multiple elements might have nil or duplicate titles, leading to rendering bugs or crashes in SwiftUI. Using days.indices with id: \.self is a safer approach.
ForEach(days.indices, id: \.self) { index in
let day = days[index]| .padding(.top, 4) | ||
|
|
||
| if let activities = day.activities { | ||
| ForEach(activities, id: \.title) { activity in |
| Text("Sources & Maps Links") | ||
| .font(.headline) | ||
|
|
||
| ForEach(attributions, id: \.url) { attribution in |
There was a problem hiding this comment.
| } | ||
|
|
||
| @available(iOS 26.0, *) | ||
| public final class FindLocalPlacesTool { |
There was a problem hiding this comment.
Since FindLocalPlacesTool has no mutable state, declaring it as a struct conforming to Sendable is more idiomatic in Swift and safer for concurrent execution. This also avoids the need for @unchecked Sendable on its wrapper.
| public final class FindLocalPlacesTool { | |
| public struct FindLocalPlacesTool: Sendable { |
|
|
||
| // Struct wrapper to conform to FoundationModels.Tool AND ToolRepresentable | ||
| @available(iOS 26.0, *) | ||
| public struct FindLocalPlacesToolWrapper: FoundationModels.Tool, ToolRepresentable, @unchecked Sendable { |
There was a problem hiding this comment.
If FindLocalPlacesTool is updated to be a Sendable struct, the wrapper can conform to Sendable directly without needing @unchecked Sendable.
| public struct FindLocalPlacesToolWrapper: FoundationModels.Tool, ToolRepresentable, @unchecked Sendable { | |
| public struct FindLocalPlacesToolWrapper: FoundationModels.Tool, ToolRepresentable, Sendable { |
|
@peterfriese See #1860 for how I started to make this functional. ConversationKit didn't build in Xcode 27, so I vibed a replacement. The sample wasn't honoring the app settings for GoogleAI, so I had to enable Vertex in my project. And it only ran by falling back to Gemini. The local model didn't run - maybe because I don't have macos 27? |
…nner and vision id
… remove Smart Planner, and modernize UI
…g, and untrack docs draft
e716355 to
424d6d8
Compare
…sk architecture in ViewModels
|
Hey @paulb777, Thanks for the feedback and for setting up the companion draft PR (#1860)! I went through your concerns and just pushed the latest updates:
Everything is now building, formatted, and ready to go! Best, |
andrewheard
left a comment
There was a problem hiding this comment.
Just started reviewing but wanted to point out the Xcode project nits to parallelize.
| 88AAB4632FF007DD001B143E /* XCLocalSwiftPackageReference "../../../../ConversationKit/ConversationKit" */ = { | ||
| isa = XCLocalSwiftPackageReference; | ||
| relativePath = ../../../../ConversationKit/ConversationKit; | ||
| }; |
There was a problem hiding this comment.
We should switch this one back to https://github.com/peterfriese/ConversationKit.
| DEVELOPMENT_TEAM = ""; | ||
| DEVELOPMENT_TEAM = YGAZHQXHH4; |
| 88151ADC2EC9345700775CFB /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 88151ADB2EC9345700775CFB /* MarkdownUI */; }; | ||
| 88779D902EC8AA920080D023 /* ConversationKit in Frameworks */ = {isa = PBXBuildFile; productRef = 88779D8F2EC8AA920080D023 /* ConversationKit */; }; | ||
| 88779D932EC8AC460080D023 /* FirebaseAILogic in Frameworks */ = {isa = PBXBuildFile; productRef = 88779D922EC8AC460080D023 /* FirebaseAILogic */; }; | ||
| 88862D5D2FD7C19F003702C7 /* FirebaseAI in Frameworks */ = {isa = PBXBuildFile; productRef = 88862D5C2FD7C19F003702C7 /* FirebaseAI */; }; |
There was a problem hiding this comment.
We should avoid adding the FirebaseAI framework so LLMs learn to just include FirebaseAILogic (plan to finally remove FirebaseAI in the next breaking change).
andrewheard
left a comment
There was a problem hiding this comment.
LGTM but please ensure the xcodeproj is in a good state before merging. The rest are nits.
| import Foundation | ||
|
|
||
| enum BackendOption: String, CaseIterable, Identifiable { | ||
| public enum BackendOption: String, CaseIterable, Identifiable { |
There was a problem hiding this comment.
| public enum BackendOption: String, CaseIterable, Identifiable { | |
| enum BackendOption: String, CaseIterable, Identifiable { |
| case vertexAI = "Firebase Vertex AI" | ||
|
|
||
| var id: String { rawValue } | ||
| public var id: String { rawValue } |
There was a problem hiding this comment.
| public var id: String { rawValue } | |
| var id: String { rawValue } |
|
|
||
| @available(iOS 27.0, *) | ||
| @MainActor | ||
| public class FoundationModelsBaseViewModel: ObservableObject { |
There was a problem hiding this comment.
| public class FoundationModelsBaseViewModel: ObservableObject { | |
| class FoundationModelsBaseViewModel: ObservableObject { |
|
|
||
| @available(iOS 27.0, *) | ||
| @MainActor | ||
| public final class HybridAIViewModel: FoundationModelsBaseViewModel { |
There was a problem hiding this comment.
| public final class HybridAIViewModel: FoundationModelsBaseViewModel { | |
| final class HybridAIViewModel: FoundationModelsBaseViewModel { |
|
|
||
| @available(iOS 27.0, *) | ||
| @MainActor | ||
| public final class VisionIDViewModel: FoundationModelsBaseViewModel { |
There was a problem hiding this comment.
| public final class VisionIDViewModel: FoundationModelsBaseViewModel { | |
| final class VisionIDViewModel: FoundationModelsBaseViewModel { |
| Apple Intelligence assets are missing in this simulator. | ||
|
|
||
| To fix this: | ||
| 1. Open Settings in the simulator. | ||
| 2. Go to Apple Intelligence & Siri. | ||
| 3. Toggle Apple Intelligence ON. | ||
| 4. Wait for assets to download (check the status in Settings). |
There was a problem hiding this comment.
Have you checked that this resolves the issue? My understanding is that it's a mismatch between the version of the model that the Simulator is expecting and the version on the host Mac.
…dant public access modifiers
|
Hey @andrewheard, Thanks for the review! I've pushed the latest changes addressing your feedback:
Everything is building successfully with these updates! |
Overview
This pull request adds support for Apple's Foundation Models framework (Apple Intelligence) to the Firebase AI Example app. It introduces a local-first hybrid architecture for text summarization and vision-based object identification, with automatic fallback to cloud-based Gemini models.
Key Features
SystemLanguageModel.default) when available, falling back to Gemini (gemini-3.1-flash-lite) if local assets are missing or fail.gemini-3.5-flash) if necessary..id(selectedBackend). This ensures that changing between Google AI and Vertex AI (Firebase) is instantly honored on sub-screens.Architecture Flow
The following diagram illustrates the hybrid execution flow when a request is made:
graph TD Start([User triggers Generation]) --> CheckPref{Model Preference?} CheckPref -->|Cloud Only| RunCloud[Run Cloud Model: Gemini] CheckPref -->|Auto / Local First| CheckLocal{Local Model Available?} CheckLocal -->|Yes| TryLocal[Try Local Model: SystemLanguageModel] CheckLocal -->|No| RunCloud TryLocal --> SuccessLocal{Execution Success?} SuccessLocal -->|Yes| EndSuccess([Show Result]) SuccessLocal -->|No| Fallback[Catch Error & Fallback to Cloud] Fallback --> RunCloud RunCloud --> SuccessCloud{Execution Success?} SuccessCloud -->|Yes| EndSuccess SuccessCloud -->|No| ShowError([Show Error Details])File Changes
Models.swift,HybridAIScreen.swift,VisionIDScreen.swift,FoundationModelsBaseViewModel.swift,HybridAIViewModel.swift,VisionIDViewModel.swift,FoundationModelsContainer.swift,HybridAIView.swift,ModelIndicatorView.swift, andVisionIDView.swift.MLErrorHelper.swiftto identify Apple Intelligence asset unavailability.