Lower deployment target from iOS 26 to iOS 18, gate Foundation Models behind @available
Broadens installable audience to iOS 18+ while keeping AI insights available on iOS 26. Foundation Models types and service wrapped in @available(iOS 26, *), InsightsViewModel conditionally instantiates the service with fallback UI on older versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import Foundation
|
||||
import FoundationModels
|
||||
|
||||
/// Represents a single AI-generated insight using @Generable for structured LLM output
|
||||
@available(iOS 26, *)
|
||||
@Generable
|
||||
struct AIInsight: Equatable {
|
||||
@Guide(description: "A brief, engaging title for the insight (3-6 words)")
|
||||
@@ -31,6 +32,7 @@ struct AIInsight: Equatable {
|
||||
}
|
||||
|
||||
/// Container for period-specific insights - the LLM generates this structure
|
||||
@available(iOS 26, *)
|
||||
@Generable
|
||||
struct AIInsightsResponse: Equatable {
|
||||
@Guide(description: "Array of exactly 5 diverse insights covering patterns, advice, and predictions")
|
||||
@@ -39,6 +41,7 @@ struct AIInsightsResponse: Equatable {
|
||||
|
||||
// MARK: - Conversion to App's Insight Model
|
||||
|
||||
@available(iOS 26, *)
|
||||
extension AIInsight {
|
||||
/// Converts AI-generated insight to the app's existing Insight model
|
||||
func toInsight() -> Insight {
|
||||
@@ -60,6 +63,7 @@ extension AIInsight {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 26, *)
|
||||
extension AIInsightsResponse {
|
||||
/// Converts all AI insights to app's Insight models
|
||||
func toInsights() -> [Insight] {
|
||||
|
||||
@@ -127,7 +127,7 @@ enum DayViewStyle: Int, CaseIterable {
|
||||
case wave = 13 // Horizontal gradient river bands
|
||||
case pattern = 14 // Mood icons as repeating background pattern
|
||||
case leather = 15 // Skeuomorphic leather with stitching
|
||||
case glass = 16 // iOS 26 liquid glass with variable blur
|
||||
case glass = 16 // Liquid glass with variable blur
|
||||
case motion = 17 // Accelerometer-driven parallax effect
|
||||
case micro = 18 // Ultra compact single-line entries
|
||||
case orbit = 19 // Celestial circular orbital arrangement
|
||||
|
||||
@@ -30,6 +30,7 @@ enum InsightGenerationError: Error, LocalizedError {
|
||||
}
|
||||
|
||||
/// Service responsible for generating AI-powered mood insights using Apple's Foundation Models
|
||||
@available(iOS 26, *)
|
||||
@MainActor
|
||||
class FoundationModelsInsightService: ObservableObject {
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ class InsightsViewModel: ObservableObject {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private let insightService = FoundationModelsInsightService()
|
||||
/// Stored as Any? to avoid referencing @available(iOS 26, *) type at the property level
|
||||
private var insightService: Any?
|
||||
private let healthService = HealthService.shared
|
||||
private let calendar = Calendar.current
|
||||
|
||||
@@ -52,7 +53,14 @@ class InsightsViewModel: ObservableObject {
|
||||
private var dataListenerToken: DataController.DataListenerToken?
|
||||
|
||||
init() {
|
||||
isAIAvailable = insightService.isAvailable
|
||||
if #available(iOS 26, *) {
|
||||
let service = FoundationModelsInsightService()
|
||||
insightService = service
|
||||
isAIAvailable = service.isAvailable
|
||||
} else {
|
||||
insightService = nil
|
||||
isAIAvailable = false
|
||||
}
|
||||
|
||||
dataListenerToken = DataController.shared.addNewDataListener { [weak self] in
|
||||
self?.onDataChanged()
|
||||
@@ -70,7 +78,9 @@ class InsightsViewModel: ObservableObject {
|
||||
/// Called when mood data changes in another tab. Invalidates cached insights
|
||||
/// so they are regenerated with fresh data on next view appearance.
|
||||
private func onDataChanged() {
|
||||
insightService.invalidateCache()
|
||||
if #available(iOS 26, *), let service = insightService as? FoundationModelsInsightService {
|
||||
service.invalidateCache()
|
||||
}
|
||||
monthLoadingState = .idle
|
||||
yearLoadingState = .idle
|
||||
allTimeLoadingState = .idle
|
||||
@@ -87,7 +97,9 @@ class InsightsViewModel: ObservableObject {
|
||||
|
||||
/// Force refresh all insights (invalidates cache)
|
||||
func refreshInsights() {
|
||||
insightService.invalidateCache()
|
||||
if #available(iOS 26, *), let service = insightService as? FoundationModelsInsightService {
|
||||
service.invalidateCache()
|
||||
}
|
||||
generateInsights()
|
||||
}
|
||||
|
||||
@@ -179,24 +191,34 @@ class InsightsViewModel: ObservableObject {
|
||||
healthAverages = healthService.computeHealthAverages(entries: validEntries, healthData: healthData)
|
||||
}
|
||||
|
||||
do {
|
||||
let insights = try await insightService.generateInsights(
|
||||
for: validEntries,
|
||||
periodName: periodName,
|
||||
count: 5,
|
||||
healthAverages: healthAverages
|
||||
)
|
||||
updateInsights(insights)
|
||||
updateState(.loaded)
|
||||
} catch {
|
||||
// On error, provide a helpful message
|
||||
if #available(iOS 26, *), let service = insightService as? FoundationModelsInsightService {
|
||||
do {
|
||||
let insights = try await service.generateInsights(
|
||||
for: validEntries,
|
||||
periodName: periodName,
|
||||
count: 5,
|
||||
healthAverages: healthAverages
|
||||
)
|
||||
updateInsights(insights)
|
||||
updateState(.loaded)
|
||||
} catch {
|
||||
// On error, provide a helpful message
|
||||
updateInsights([Insight(
|
||||
icon: "exclamationmark.triangle",
|
||||
title: "Insights Unavailable",
|
||||
description: "Unable to generate AI insights right now. Please try again later.",
|
||||
mood: nil
|
||||
)])
|
||||
updateState(.error(error.localizedDescription))
|
||||
}
|
||||
} else {
|
||||
updateInsights([Insight(
|
||||
icon: "exclamationmark.triangle",
|
||||
title: "Insights Unavailable",
|
||||
description: "Unable to generate AI insights right now. Please try again later.",
|
||||
icon: "brain.head.profile",
|
||||
title: "AI Unavailable",
|
||||
description: "Apple Intelligence is required for personalized insights. Please enable it in Settings.",
|
||||
mood: nil
|
||||
)])
|
||||
updateState(.error(error.localizedDescription))
|
||||
updateState(.error("AI not available"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user