Apple's Foundation Models framework (iOS 18.2+) exposes the on-device LLM that powers Apple Intelligence. For developers, the most useful capability isn't chat — it's schema-driven generation: ask the model for a value matching a Swift type, and it returns a validated instance you can use directly, no JSON parsing or prompt-shape gymnastics.
- iOS 18.2+ deployment target.
- A device that supports Apple Intelligence (iPhone 15 Pro / 16 family, M-series iPad, M-series Mac).
- Apple Intelligence enabled in Settings — the user controls this.
import FoundationModelsin your Swift target.
Step 1Check availability first
The framework is always importable, but the model isn't always usable — old device, region restriction, Apple Intelligence disabled, model still downloading. Guard at the top of any feature that needs it.
import FoundationModels enum AICapability { static var isAvailable: Bool { SystemLanguageModel.default.availability == .available } static var reasonUnavailable: String? { switch SystemLanguageModel.default.availability { case .available: return nil case .unavailable(.deviceNotEligible): return "Needs Apple Intelligence hardware." case .unavailable(.appleIntelligenceNotEnabled): return "Turn on Apple Intelligence in Settings." case .unavailable(.modelNotReady): return "Model is still downloading." @unknown default: return "Unavailable." } } }
Step 2Define your output schema
The Swift compiler does the heavy lifting via @Generable. Annotate the type and its properties; the framework gives the model a schema that matches.
@Generable struct CampSuggestion { @Guide(description: "Park or area name") let name: String @Guide(description: "Why this fits the user's request") let rationale: String @Guide(description: "Drive time in hours from the user's location", .range(0.5...6.0)) let driveTimeHours: Double @Guide(description: "Best season for this kind of trip", .anyOf(["spring", "summer", "fall", "winter"])) let bestSeason: String } @Generable struct CampSuggestionList { @Guide(description: "3 distinct candidates, ranked best-to-worst") let suggestions: [CampSuggestion] }
Step 3Run a session
func suggestCamps(for prompt: String) async throws -> CampSuggestionList { let session = LanguageModelSession(instructions: Instructions { "You are a camping-trip recommender for users in the central US." "Recommend state parks and national forests. Be specific and brief." }) let response = try await session.respond(to: prompt, generating: CampSuggestionList.self) return response.content }
That's it. response.content is a typed CampSuggestionList. The framework handles prompting the model with the schema, parsing its output, retrying on validation failure, and giving you a clean value. No JSON, no manual prompt engineering.
Step 4Stream the response
For longer outputs, stream so the UI updates as the model generates.
let stream = session.streamResponse(to: prompt, generating: CampSuggestionList.self) for try await partial in stream { await MainActor.run { self.partial = partial } }
partial is an evolving CampSuggestionList.PartiallyGenerated — fields appear as the model fills them. Bind it to a SwiftUI @State and the list grows in real time.
Step 5Tool calls (let the model query your data)
Sometimes the model needs to look up something your app knows. Define a Tool and the model decides when to invoke it.
struct CurrentWeatherTool: Tool { let name = "current_weather" let description = "Get the current weather at a park." @Generable struct Arguments { @Guide(description: "Park name") let park: String } func call(arguments: Arguments) async throws -> ToolOutput { let w = try await WeatherService.shared.current(for: arguments.park) return .init("Currently \(w.tempF)°F, \(w.conditions).") } } let session = LanguageModelSession(tools: [CurrentWeatherTool()]) { ... }
Treat the on-device model the way you'd treat a junior employee with strong instincts and no context. Give it the goal in plain English, the constraints, and ideally an example. Resist the urge to engineer the prompt — schema-driven generation handles most of the formatting work for you.
Step 6Graceful fallback
Anything that uses Foundation Models needs a non-AI fallback because most iOS devices still don't support Apple Intelligence. The right pattern: have the AI feature enhance an experience that already works without it.
if AICapability.isAvailable { // AI-powered Discover with natural-language prompts AIDiscoverView() } else { // Filter-based Discover — works on every device FilteredDiscoverView() }
This is the rule that's saved the most pain: the app must do its job without the AI. AI is the enhancement, not the foundation.
- Works on your device, fails everywhere else. You're testing on hardware with Apple Intelligence and the user isn't. Always test the
.unavailablebranch. - Model returns nonsense for novel categories. Add an example in your Instructions block. The on-device model is smaller than cloud LLMs and benefits more from in-context guidance.
- Schema validation fails repeatedly. Your
@Guidehints are conflicting (e.g. range + anyOf that don't intersect). Simplify the schema. - First call is slow. The model warms up. Pre-warm in the background after first launch (
_ = LanguageModelSession()). - The user disabled Apple Intelligence between launches. Re-check availability before every session — don't cache the result for the app lifetime.