diff --git a/firebaseai/FirebaseAIExample/ContentView.swift b/firebaseai/FirebaseAIExample/ContentView.swift index 2ca70ebad..11ae44124 100644 --- a/firebaseai/FirebaseAIExample/ContentView.swift +++ b/firebaseai/FirebaseAIExample/ContentView.swift @@ -130,17 +130,25 @@ struct ContentView: View { case "LiveScreen": LiveScreen(backendType: selectedBackend, sample: sample) case "FoundationModelsHybrid": - if #available(iOS 27.0, *) { - HybridAIScreen(backendType: selectedBackend) - } else { - Text("Foundation Models are only available on iOS 27 or newer.") - } + #if canImport(FoundationModels) + if #available(iOS 27.0, *) { + HybridAIScreen(backendType: selectedBackend) + } else { + Text("Foundation Models are only available on iOS 27 or newer.") + } + #else + Text("Foundation Models are not supported on this platform/SDK.") + #endif case "FoundationModelsVisionID": - if #available(iOS 27.0, *) { - VisionIDScreen(backendType: selectedBackend) - } else { - Text("Foundation Models are only available on iOS 27 or newer.") - } + #if canImport(FoundationModels) + if #available(iOS 27.0, *) { + VisionIDScreen(backendType: selectedBackend) + } else { + Text("Foundation Models are only available on iOS 27 or newer.") + } + #else + Text("Foundation Models are not supported on this platform/SDK.") + #endif default: EmptyView() } diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Models/Models.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Models/Models.swift index 555917375..adddeb797 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Models/Models.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Models/Models.swift @@ -12,27 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import FoundationModels +#if canImport(FoundationModels) + import Foundation + import FoundationModels -@available(iOS 26.0, *) -@Generable -struct IdentifiedObject: Equatable, Codable { - @Guide(description: "The name of the primary object or landmark detected.") - let name: String + @available(iOS 26.0, *) + @Generable + struct IdentifiedObject: Equatable, Codable { + @Guide(description: "The name of the primary object or landmark detected.") + let name: String - @Guide( - description: "The category of the object (e.g. Landmark, Plant, Food, Animal, Device, Clothing)." - ) - let category: String + @Guide( + description: "The category of the object (e.g. Landmark, Plant, Food, Animal, Device, Clothing)." + ) + let category: String - @Guide(description: "A short, 2-sentence description of the object and interesting facts.") - let description: String -} + @Guide(description: "A short, 2-sentence description of the object and interesting facts.") + let description: String + } -@available(iOS 26.0, *) -@Generable -struct TextSummary: Equatable, Codable { - @Guide(description: "A list of exactly 2 key summary points.") - let summaryPoints: [String] -} + @available(iOS 26.0, *) + @Generable + struct TextSummary: Equatable, Codable { + @Guide(description: "A list of exactly 2 key summary points.") + let summaryPoints: [String] + } +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/HybridAIScreen.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/HybridAIScreen.swift index 45b4fa64f..95e874221 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/HybridAIScreen.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/HybridAIScreen.swift @@ -12,19 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI +#if canImport(FoundationModels) + import SwiftUI -@available(iOS 27.0, *) -struct HybridAIScreen: View { - @StateObject private var viewModel: HybridAIViewModel + @available(iOS 27.0, *) + struct HybridAIScreen: View { + @StateObject private var viewModel: HybridAIViewModel - init(backendType: BackendOption) { - _viewModel = StateObject(wrappedValue: HybridAIViewModel(backendType: backendType)) - } + init(backendType: BackendOption) { + _viewModel = StateObject(wrappedValue: HybridAIViewModel(backendType: backendType)) + } - var body: some View { - FoundationModelsContainer(viewModel: viewModel, title: "Hybrid AI") { vm in - HybridAIView(viewModel: vm) + var body: some View { + FoundationModelsContainer(viewModel: viewModel, title: "Hybrid AI") { vm in + HybridAIView(viewModel: vm) + } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/VisionIDScreen.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/VisionIDScreen.swift index 83b133465..3b7623f6a 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/VisionIDScreen.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Screens/VisionIDScreen.swift @@ -12,21 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI -import PhotosUI +#if canImport(FoundationModels) + import SwiftUI + import PhotosUI -@available(iOS 27.0, *) -struct VisionIDScreen: View { - @StateObject private var viewModel: VisionIDViewModel - @State private var photosPickerItem: PhotosPickerItem? = nil + @available(iOS 27.0, *) + struct VisionIDScreen: View { + @StateObject private var viewModel: VisionIDViewModel + @State private var photosPickerItem: PhotosPickerItem? = nil - init(backendType: BackendOption) { - _viewModel = StateObject(wrappedValue: VisionIDViewModel(backendType: backendType)) - } + init(backendType: BackendOption) { + _viewModel = StateObject(wrappedValue: VisionIDViewModel(backendType: backendType)) + } - var body: some View { - FoundationModelsContainer(viewModel: viewModel, title: "Vision ID") { vm in - VisionIDView(viewModel: vm, photosPickerItem: $photosPickerItem) + var body: some View { + FoundationModelsContainer(viewModel: viewModel, title: "Vision ID") { vm in + VisionIDView(viewModel: vm, photosPickerItem: $photosPickerItem) + } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/FoundationModelsBaseViewModel.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/FoundationModelsBaseViewModel.swift index b6706505e..3bb34ae13 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/FoundationModelsBaseViewModel.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/FoundationModelsBaseViewModel.swift @@ -12,47 +12,49 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import SwiftUI -import Combine -import FoundationModels -import FirebaseAILogic - -enum ModelPreference: String, CaseIterable, Identifiable { - case auto = "Auto (Local First)" - case cloud = "Cloud Only" - - var id: String { rawValue } -} - -@available(iOS 27.0, *) -@MainActor -class FoundationModelsBaseViewModel: ObservableObject { - @Published var inProgress = false - @Published var error: Error? - @Published var modelPreference: ModelPreference = .auto - @Published var isUsingLocalModel: Bool = false - - internal var activeTask: Task? - let backendType: BackendOption - - init(backendType: BackendOption) { - self.backendType = backendType +#if canImport(FoundationModels) + import Foundation + import SwiftUI + import Combine + import FoundationModels + import FirebaseAILogic + + enum ModelPreference: String, CaseIterable, Identifiable { + case auto = "Auto (Local First)" + case cloud = "Cloud Only" + + var id: String { rawValue } } - func stopActiveTask() { - activeTask?.cancel() - activeTask = nil - inProgress = false - } + @available(iOS 27.0, *) + @MainActor + class FoundationModelsBaseViewModel: ObservableObject { + @Published var inProgress = false + @Published var error: Error? + @Published var modelPreference: ModelPreference = .auto + @Published var isUsingLocalModel: Bool = false + + internal var activeTask: Task? + let backendType: BackendOption + + init(backendType: BackendOption) { + self.backendType = backendType + } + + func stopActiveTask() { + activeTask?.cancel() + activeTask = nil + inProgress = false + } - internal func getFirebaseAI() -> FirebaseAI { - switch backendType { - case .googleAI: - return FirebaseAI.firebaseAI(backend: .googleAI()) - case .vertexAI: - // Using "global" as location for Foundation Models fallback compatibility - return FirebaseAI.firebaseAI(backend: .vertexAI(location: "global")) + internal func getFirebaseAI() -> FirebaseAI { + switch backendType { + case .googleAI: + return FirebaseAI.firebaseAI(backend: .googleAI()) + case .vertexAI: + // Using "global" as location for Foundation Models fallback compatibility + return FirebaseAI.firebaseAI(backend: .vertexAI(location: "global")) + } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/HybridAIViewModel.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/HybridAIViewModel.swift index 735d9b09c..f0bd2cbc7 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/HybridAIViewModel.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/HybridAIViewModel.swift @@ -12,40 +12,70 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import SwiftUI -import Combine -import FoundationModels -import FirebaseAILogic +#if canImport(FoundationModels) + import Foundation + import SwiftUI + import Combine + import FoundationModels + import FirebaseAILogic -@available(iOS 27.0, *) -@MainActor -final class HybridAIViewModel: FoundationModelsBaseViewModel { - @Published var inputText: String = - "It is the quintessential autumn harvest fruit, famously baked into warm cinnamon pastries, dipped in sticky caramel on Halloween, and traditionally rumored to keep medical professionals at bay if eaten once a day." - @Published var outputSummary: TextSummary? + @available(iOS 27.0, *) + @MainActor + final class HybridAIViewModel: FoundationModelsBaseViewModel { + @Published var inputText: String = + "It is the quintessential autumn harvest fruit, famously baked into warm cinnamon pastries, dipped in sticky caramel on Halloween, and traditionally rumored to keep medical professionals at bay if eaten once a day." + @Published var outputSummary: TextSummary? - func runSummarization() { - stopActiveTask() - inProgress = true - error = nil - outputSummary = nil + func runSummarization() { + stopActiveTask() + inProgress = true + error = nil + outputSummary = nil - activeTask = Task { - defer { self.inProgress = false } + activeTask = Task { + defer { self.inProgress = false } - let instructions = Instructions { - "Your job is to summarize the provided text in exactly 2 bullet points." - } + let instructions = Instructions { + "Your job is to summarize the provided text in exactly 2 bullet points." + } - let availability = SystemLanguageModel.default.availability + let availability = SystemLanguageModel.default.availability + + // Try local model first if it reports available and not forced to cloud + if modelPreference == .auto, availability == .available { + isUsingLocalModel = true + do { + let session = LanguageModelSession( + model: SystemLanguageModel.default, + instructions: instructions + ) + let response = try await session.respond( + to: inputText, + generating: TextSummary.self + ) + if !Task.isCancelled { + self.outputSummary = response.content + } + return + } catch { + // Fall back to cloud model if local model fails + if error.isMLAssetUnavailable { + print("Local ML assets unavailable. Falling back to cloud model...") + } else { + print( + "Local model failed: \(error.localizedDescription). Falling back to cloud model..." + ) + } + } + } - // Try local model first if it reports available and not forced to cloud - if modelPreference == .auto, availability == .available { - isUsingLocalModel = true + // Fallback to cloud model + isUsingLocalModel = false do { + let ai = getFirebaseAI() + let model = ai.geminiLanguageModel(name: "gemini-3.1-flash-lite") let session = LanguageModelSession( - model: SystemLanguageModel.default, + model: model, instructions: instructions ) let response = try await session.respond( @@ -55,40 +85,12 @@ final class HybridAIViewModel: FoundationModelsBaseViewModel { if !Task.isCancelled { self.outputSummary = response.content } - return } catch { - // Fall back to cloud model if local model fails - if error.isMLAssetUnavailable { - print("Local ML assets unavailable. Falling back to cloud model...") - } else { - print( - "Local model failed: \(error.localizedDescription). Falling back to cloud model..." - ) + if !Task.isCancelled { + self.error = error } } } - - // Fallback to cloud model - isUsingLocalModel = false - do { - let ai = getFirebaseAI() - let model = ai.geminiLanguageModel(name: "gemini-3.1-flash-lite") - let session = LanguageModelSession( - model: model, - instructions: instructions - ) - let response = try await session.respond( - to: inputText, - generating: TextSummary.self - ) - if !Task.isCancelled { - self.outputSummary = response.content - } - } catch { - if !Task.isCancelled { - self.error = error - } - } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/VisionIDViewModel.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/VisionIDViewModel.swift index b3fa7f2ab..1d3bbbed1 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/VisionIDViewModel.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/ViewModels/VisionIDViewModel.swift @@ -12,43 +12,76 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import SwiftUI -import Combine -import FoundationModels -import FirebaseAILogic +#if canImport(FoundationModels) + import Foundation + import SwiftUI + import Combine + import FoundationModels + import FirebaseAILogic -@available(iOS 27.0, *) -@MainActor -final class VisionIDViewModel: FoundationModelsBaseViewModel { - @Published var selectedImage: UIImage? - @Published var identifiedObject: IdentifiedObject? + @available(iOS 27.0, *) + @MainActor + final class VisionIDViewModel: FoundationModelsBaseViewModel { + @Published var selectedImage: UIImage? + @Published var identifiedObject: IdentifiedObject? - func identifySelectedImage() { - guard let image = selectedImage else { return } - stopActiveTask() - inProgress = true - error = nil - identifiedObject = nil - isUsingLocalModel = false + func identifySelectedImage() { + guard let image = selectedImage else { return } + stopActiveTask() + inProgress = true + error = nil + identifiedObject = nil + isUsingLocalModel = false - activeTask = Task { - defer { self.inProgress = false } + activeTask = Task { + defer { self.inProgress = false } - guard let cgImage = image.cgImage else { return } + guard let cgImage = image.cgImage else { return } - let instructions = Instructions { - "You are a visual object identifier." - } + let instructions = Instructions { + "You are a visual object identifier." + } - let availability = SystemLanguageModel.default.availability + let availability = SystemLanguageModel.default.availability - // Try local model first if it reports available and not forced to cloud - if modelPreference == .auto, availability == .available { - isUsingLocalModel = true + // Try local model first if it reports available and not forced to cloud + if modelPreference == .auto, availability == .available { + isUsingLocalModel = true + do { + let session = LanguageModelSession( + model: SystemLanguageModel.default, + instructions: instructions + ) + let response = try await session.respond( + generating: IdentifiedObject.self + ) { + "Identify the primary object in this image. Be as specific as possible, categorize it, and provide a short 2-sentence description." + Attachment(cgImage).label("image") + } + if !Task.isCancelled { + self.identifiedObject = response.content + } + return + } catch { + if error.isMLAssetUnavailable { + print( + "Local ML assets unavailable for Vision ID. Falling back to cloud model..." + ) + } else { + print( + "Local model failed for Vision ID: \(error.localizedDescription). Falling back to cloud model..." + ) + } + } + } + + // Fallback to cloud model + isUsingLocalModel = false do { + let ai = getFirebaseAI() + let model = ai.geminiLanguageModel(name: "gemini-3.5-flash") let session = LanguageModelSession( - model: SystemLanguageModel.default, + model: model, instructions: instructions ) let response = try await session.respond( @@ -57,47 +90,16 @@ final class VisionIDViewModel: FoundationModelsBaseViewModel { "Identify the primary object in this image. Be as specific as possible, categorize it, and provide a short 2-sentence description." Attachment(cgImage).label("image") } + if !Task.isCancelled { self.identifiedObject = response.content } - return } catch { - if error.isMLAssetUnavailable { - print( - "Local ML assets unavailable for Vision ID. Falling back to cloud model..." - ) - } else { - print( - "Local model failed for Vision ID: \(error.localizedDescription). Falling back to cloud model..." - ) + if !Task.isCancelled { + self.error = error } } } - - // Fallback to cloud model - isUsingLocalModel = false - do { - let ai = getFirebaseAI() - let model = ai.geminiLanguageModel(name: "gemini-3.5-flash") - let session = LanguageModelSession( - model: model, - instructions: instructions - ) - let response = try await session.respond( - generating: IdentifiedObject.self - ) { - "Identify the primary object in this image. Be as specific as possible, categorize it, and provide a short 2-sentence description." - Attachment(cgImage).label("image") - } - - if !Task.isCancelled { - self.identifiedObject = response.content - } - } catch { - if !Task.isCancelled { - self.error = error - } - } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/FoundationModelsContainer.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/FoundationModelsContainer.swift index 40c2f09ad..6085d36b4 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/FoundationModelsContainer.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/FoundationModelsContainer.swift @@ -12,61 +12,63 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI +#if canImport(FoundationModels) + import SwiftUI -@available(iOS 27.0, *) -struct FoundationModelsContainer: View { - @ObservedObject var viewModel: VM - @State private var presentErrorDetails = false - let title: String - let content: (VM) -> Content + @available(iOS 27.0, *) + struct FoundationModelsContainer: View { + @ObservedObject var viewModel: VM + @State private var presentErrorDetails = false + let title: String + let content: (VM) -> Content - var body: some View { - ZStack { - ScrollView { - content(viewModel) - .padding() - .frame(maxWidth: .infinity, alignment: .topLeading) - } + var body: some View { + ZStack { + ScrollView { + content(viewModel) + .padding() + .frame(maxWidth: .infinity, alignment: .topLeading) + } - if viewModel.inProgress { - ProgressOverlay() - } - } - .background(Color(.systemGroupedBackground)) - .navigationTitle(title) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItemGroup(placement: .topBarTrailing) { if viewModel.inProgress { - Button(action: { - viewModel.stopActiveTask() - }) { - Image(systemName: "stop.circle") - .font(.title3) - } + ProgressOverlay() } + } + .background(Color(.systemGroupedBackground)) + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + if viewModel.inProgress { + Button(action: { + viewModel.stopActiveTask() + }) { + Image(systemName: "stop.circle") + .font(.title3) + } + } - Menu { - Picker("Model Preference", selection: $viewModel.modelPreference) { - ForEach(ModelPreference.allCases) { pref in - Text(pref.rawValue).tag(pref) + Menu { + Picker("Model Preference", selection: $viewModel.modelPreference) { + ForEach(ModelPreference.allCases) { pref in + Text(pref.rawValue).tag(pref) + } } + } label: { + Image(systemName: "cpu") } - } label: { - Image(systemName: "cpu") } } - } - .sheet(isPresented: $presentErrorDetails) { - if let error = viewModel.error { - ErrorDetailsView(error: error) + .sheet(isPresented: $presentErrorDetails) { + if let error = viewModel.error { + ErrorDetailsView(error: error) + } } - } - .onChange(of: viewModel.error != nil) { oldValue, newValue in - if newValue { - presentErrorDetails = true + .onChange(of: viewModel.error != nil) { oldValue, newValue in + if newValue { + presentErrorDetails = true + } } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/HybridAIView.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/HybridAIView.swift index 7ca097e82..7eaacd696 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/HybridAIView.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/HybridAIView.swift @@ -12,67 +12,69 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI +#if canImport(FoundationModels) + import SwiftUI -@available(iOS 27.0, *) -struct HybridAIView: View { - @ObservedObject var viewModel: HybridAIViewModel + @available(iOS 27.0, *) + struct HybridAIView: View { + @ObservedObject var viewModel: HybridAIViewModel - var body: some View { - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Input Text") - .font(.headline) - TextEditor(text: $viewModel.inputText) - .frame(height: 120) - .padding(4) - .background(Color(.secondarySystemGroupedBackground)) - .cornerRadius(8) - } - - Button(action: { - viewModel.runSummarization() - }) { - HStack { - Spacer() - Image(systemName: "sparkles") - Text("Summarise") - Spacer() + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text("Input Text") + .font(.headline) + TextEditor(text: $viewModel.inputText) + .frame(height: 120) + .padding(4) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(8) } - .padding() - .foregroundColor(.white) - .background(Color.blue) - .cornerRadius(10) - } - .disabled(viewModel.inProgress) - if let summary = viewModel.outputSummary { - VStack(alignment: .leading, spacing: 12) { + Button(action: { + viewModel.runSummarization() + }) { HStack { - Text("Summary Points") - .font(.headline) Spacer() - - ModelIndicatorView(isUsingLocalModel: viewModel.isUsingLocalModel) + Image(systemName: "sparkles") + Text("Summarise") + Spacer() } + .padding() + .foregroundColor(.white) + .background(Color.blue) + .cornerRadius(10) + } + .disabled(viewModel.inProgress) + + if let summary = viewModel.outputSummary { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Summary Points") + .font(.headline) + Spacer() + + ModelIndicatorView(isUsingLocalModel: viewModel.isUsingLocalModel) + } - VStack(alignment: .leading, spacing: 8) { - ForEach(summary.summaryPoints, id: \.self) { point in - HStack(alignment: .top, spacing: 6) { - Text("•") - .bold() - Text(point) - .font(.subheadline) + VStack(alignment: .leading, spacing: 8) { + ForEach(summary.summaryPoints, id: \.self) { point in + HStack(alignment: .top, spacing: 6) { + Text("•") + .bold() + Text(point) + .font(.subheadline) + } } } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(10) } - .padding() - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.secondarySystemGroupedBackground)) - .cornerRadius(10) + .transition(.opacity.combined(with: .slide)) } - .transition(.opacity.combined(with: .slide)) } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/VisionIDView.swift b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/VisionIDView.swift index 6a8d7516a..7ef0d801d 100644 --- a/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/VisionIDView.swift +++ b/firebaseai/FirebaseAIExample/Features/AppleFoundationModels/Views/VisionIDView.swift @@ -12,88 +12,90 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftUI -import PhotosUI - -@available(iOS 27.0, *) -@MainActor -struct VisionIDView: View { - @ObservedObject var viewModel: VisionIDViewModel - @Binding var photosPickerItem: PhotosPickerItem? +#if canImport(FoundationModels) + import SwiftUI + import PhotosUI + @available(iOS 27.0, *) @MainActor - var body: some View { - let selectedImage = viewModel.selectedImage + struct VisionIDView: View { + @ObservedObject var viewModel: VisionIDViewModel + @Binding var photosPickerItem: PhotosPickerItem? + + @MainActor + var body: some View { + let selectedImage = viewModel.selectedImage - VStack(alignment: .leading, spacing: 16) { - Text("Select or Snap a Photo to Identify") - .font(.headline) + VStack(alignment: .leading, spacing: 16) { + Text("Select or Snap a Photo to Identify") + .font(.headline) - PhotosPicker(selection: $photosPickerItem, matching: .images) { - VStack(spacing: 12) { - if let image = selectedImage { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 200) + PhotosPicker(selection: $photosPickerItem, matching: .images) { + VStack(spacing: 12) { + if let image = selectedImage { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 200) + .cornerRadius(12) + } else { + VStack(spacing: 8) { + Image(systemName: "photo.badge.plus") + .font(.system(size: 40)) + Text("Select an Image") + } + .frame(maxWidth: .infinity) + .frame(height: 180) + .background(Color(.secondarySystemGroupedBackground)) .cornerRadius(12) - } else { - VStack(spacing: 8) { - Image(systemName: "photo.badge.plus") - .font(.system(size: 40)) - Text("Select an Image") } - .frame(maxWidth: .infinity) - .frame(height: 180) - .background(Color(.secondarySystemGroupedBackground)) - .cornerRadius(12) } } - } - .onChange(of: photosPickerItem) { oldItem, newItem in - Task { - if let data = try? await newItem?.loadTransferable(type: Data.self), - let image = UIImage(data: data) { - viewModel.selectedImage = image - viewModel.identifySelectedImage() + .onChange(of: photosPickerItem) { oldItem, newItem in + Task { + if let data = try? await newItem?.loadTransferable(type: Data.self), + let image = UIImage(data: data) { + viewModel.selectedImage = image + viewModel.identifySelectedImage() + } } } - } - if let identified = viewModel.identifiedObject { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Identification Result") - .font(.headline) - Spacer() - ModelIndicatorView(isUsingLocalModel: viewModel.isUsingLocalModel) - } - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Name:") - .bold() - Text(identified.name) - } + if let identified = viewModel.identifiedObject { + VStack(alignment: .leading, spacing: 12) { HStack { - Text("Category:") - .bold() - Text(identified.category) + Text("Identification Result") + .font(.headline) + Spacer() + ModelIndicatorView(isUsingLocalModel: viewModel.isUsingLocalModel) } - VStack(alignment: .leading, spacing: 4) { - Text("Description:") - .bold() - Text(identified.description) - .foregroundColor(.secondary) + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Name:") + .bold() + Text(identified.name) + } + HStack { + Text("Category:") + .bold() + Text(identified.category) + } + VStack(alignment: .leading, spacing: 4) { + Text("Description:") + .bold() + Text(identified.description) + .foregroundColor(.secondary) + } } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(10) } - .padding() - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(.secondarySystemGroupedBackground)) - .cornerRadius(10) + .transition(.opacity) } - .transition(.opacity) } } } -} +#endif diff --git a/firebaseai/FirebaseAIExample/Shared/Util/MLErrorHelper.swift b/firebaseai/FirebaseAIExample/Shared/Util/MLErrorHelper.swift index 2a34678e0..615391700 100644 --- a/firebaseai/FirebaseAIExample/Shared/Util/MLErrorHelper.swift +++ b/firebaseai/FirebaseAIExample/Shared/Util/MLErrorHelper.swift @@ -13,20 +13,24 @@ // limitations under the License. import Foundation -import FoundationModels +#if canImport(FoundationModels) + import FoundationModels +#endif extension Error { /// Returns true if this error indicates that required ML assets (like safety guardrails) /// are missing from the device or simulator. var isMLAssetUnavailable: Bool { // 1. Check for LanguageModelSession.GenerationError.assetsUnavailable - if #available(iOS 26.0, *) { - if let genError = self as? LanguageModelSession.GenerationError { - if case .assetsUnavailable = genError { - return true + #if canImport(FoundationModels) + if #available(iOS 26.0, *) { + if let genError = self as? LanguageModelSession.GenerationError { + if case .assetsUnavailable = genError { + return true + } } } - } + #endif let nsError = self as NSError