On-device AI with Foundation Models (iOS 26)
Apple's Foundation Models framework
(WWDC25) exposes the on-device LLM behind Apple Intelligence to your app. You always gate on
availability first, then run a LanguageModelSession,
get type-safe output via guided generation
(@Generable +
@Guide), stream partial results, and optionally extend the model with the
Tool protocol. This is a
hardware-gated capability, not just a UI pattern — the framework leads with availability for a
reason.
When to invoke
- You're adding on-device generation/understanding: summarization, content tagging, smart search, suggested replies, structured extraction — and want it private and offline.
- You need structured output (a Swift type, not a raw string) from the model.
- You must fail soft on devices without Apple Intelligence.
Announce on invoke: "Using swift-ondevice-ai-language-model-patterns to gate availability, run a LanguageModelSession, and generate @Generable output."
Do not reach for this for tasks needing world knowledge, long context, or guaranteed availability — the on-device model is small (4,096-token window) and absent on older hardware. For those, a server model behind your own API is the right call.
The canonical APIs (verified)
| API | Signature / form (verified) | Use |
|---|---|---|
SystemLanguageModel.default |
static var default: SystemLanguageModel |
The on-device model handle |
.availability |
var availability: SystemLanguageModel.Availability → .available / .unavailable(UnavailableReason) |
The mandatory pre-flight gate |
UnavailableReason |
.appleIntelligenceNotEnabled, .deviceNotEligible, .modelNotReady |
Why it's off |
LanguageModelSession |
final class; init(instructions:), init(model:tools:instructions:) |
A stateful generation session |
respond(to:options:) |
returns LanguageModelSession.Response<String> |
One-shot text response |
respond(generating:includeSchemaInPrompt:options:prompt:) |
returns Response<Content> (guided) |
Structured output |
streamResponse(to:generating:includeSchemaInPrompt:options:) |
returns LanguageModelSession.ResponseStream<Content> |
Streamed structured output |
@Generable |
macro → Generable : ConvertibleFromGeneratedContent, ConvertibleToGeneratedContent |
Mark a type the model can produce |
@Guide(description:…) |
property macro; supports guides like .count(_:), .range(_:) |
Constrain / describe a field |
Tool |
protocol Tool<Arguments, Output> : Sendable; call(arguments:) |
Function calling |
GenerationOptions |
maximumResponseTokens, sampling, temperature |
Tune the request |
The rules (load-bearing)
1. Gate on SystemLanguageModel.default.availability BEFORE constructing a session
Constructing/using a session on an unsupported device fails. Check availability and branch to a fallback. The unavailable reasons are actionable (prompt the user to enable Apple Intelligence vs. hide the feature on ineligible hardware):
switch SystemLanguageModel.default.availability {
case .available:
// proceed
case .unavailable(.appleIntelligenceNotEnabled):
// deep-link to Settings, or show "turn on Apple Intelligence"
case .unavailable(.deviceNotEligible), .unavailable(.modelNotReady):
// hide the feature / degrade gracefully
}
2. Structured output = a @Generable type with @Guided fields
Annotate the type with @Generable; describe properties with @Guide. The framework uses
constrained sampling so the model can't produce malformed output. Keep descriptions short — they
consume the context window.
@Generable
struct SearchSuggestions {
@Guide(description: "Suggested search terms.", .count(4))
var terms: [String]
}
3. Streaming yields a ResponseStream of partial snapshots — handle progressive reveal
streamResponse(...) returns a LanguageModelSession.ResponseStream<Content>; iterate it for
Snapshots where the generated content is filled in progressively (a PartiallyGenerated mirror
with optional fields). Your UI must render half-filled state, then settle. Call .collect() to await
the final value instead.
4. The context window is 4,096 tokens — split large work into sessions
Instructions + all prompts + all outputs share one 4,096-token budget. Exceeding it throws
exceededContextWindowSize(_:). For long inputs, chunk the work and run each chunk in a new
LanguageModelSession, then combine. Tool definitions also consume the window.
5. Tools must be Sendable; the model decides when to call them
A Tool carries a name + description (the model uses them to decide invocation) and a
call(arguments:) whose Arguments are themselves @Generable. Tools run concurrently, so the
protocol requires Sendable.
6. Wrap it in a @MainActor @Observable capability provider
Surface availability and streamed state to the View layer through an @Observable object on the main
actor. The same provider can gate other Apple Intelligence features (Writing Tools, Image Playground)
behind one availability check.
Canonical example
import FoundationModels
@Generable
struct Recipe: Sendable {
@Guide(description: "The recipe title.") var title: String
@Guide(description: "Ingredients with quantities.") var ingredients: [String]
@Guide(description: "Ordered preparation steps.") var steps: [String]
}
@MainActor @Observable
final class RecipeGenerator {
enum State { case unsupported(SystemLanguageModel.Availability), idle, streaming(Recipe.PartiallyGenerated), done(Recipe) }
private(set) var state: State = .idle
func start() {
if case .unavailable = SystemLanguageModel.default.availability {
state = .unsupported(SystemLanguageModel.default.availability); return // fail soft
}
state = .idle
}
func generate(prompt: String) async {
guard case .available = SystemLanguageModel.default.availability else { return }
let session = LanguageModelSession(
instructions: "You are a chef. Produce a concise, safe recipe."
)
do {
let stream = session.streamResponse(to: prompt, generating: Recipe.self)
for try await snapshot in stream { // progressive reveal
state = .streaming(snapshot.content)
}
let final = try await stream.collect()
state = .done(final.content)
} catch {
// includes exceededContextWindowSize — start a new session with a shorter prompt
}
}
}
Decision aid: when NOT to / trade-offs
- Not for world-knowledge or long context. 4,096 tokens and a ~3B on-device model — use a server model when you need breadth or large inputs.
- Availability is a moving target at runtime. Battery, Game Mode, or a still-downloading model can
flip it to
.modelNotReady; re-check, don't cache "available" forever. - Don't ship a feature that hard-fails on A16 and older. Those devices report
.deviceNotEligible— design the fallback (hide, or route to a server) as a first-class path.
Related skills
global-skills/apple/swift-clean-architecture-module-scaffold/SKILL.md— the@MainActor@Observablecapability provider andSendableTool isolation.global-skills/apple/swift-feature-scaffold-mvvm-clean-arch/SKILL.md— wiring the streamed state into a feature'sStateenum.global-skills/meta/skill-pattern-freshness-audit/SKILL.md— re-verify Foundation Models symbols after each WWDC (the model + API surface is new and evolving).
Sources
- Foundation Models · LanguageModelSession · SystemLanguageModel · Generable · Tool · Generating Swift data structures with guided generation
- WWDC25 286 Meet the Foundation Models framework · 259 Code-along: Bring on-device AI to your app
Last verified: 2026-06-03 against Apple Developer docs (live). Resolved draft open questions:
@Generable/@Guide confirmed real (Generable protocol page, sample uses .count(4)/.range(1...10));
streamResponse(to:generating:includeSchemaInPrompt:options:) returns ResponseStream<Content>; Tool
is protocol Tool<Arguments, Output> : Sendable; the on-device context window is 4,096 tokens (Apple
docs), correcting the ~2000 estimate.
Re-check after: WWDC26, or by 2026-12-01. Decay risk: medium (new framework; model size and API
surface may shift).
Found a drift? Run /skill-pattern-freshness-audit apple.
Source: esaldgut/ai-native-engineering-workspace — distributed by TomeVault.