← All release notes
SwiftiOS 26Aug 17, 2026 · 5 min read

The Foundation Models Framework — On-Device LLM Access for Apps

iOS 26's Foundation Models framework gives apps on-device LLM access with typed, structured output — no API key, no network call.

iOS 26 ships direct access to the on-device large language model behind Apple Intelligence via the new Foundation Models framework. It's a real model with real limitations — no server calls, no open-ended chat product, a much smaller model than the ones behind server-scale assistants — but for structured, in-app tasks it removes a lot of boilerplate: no API key, no network dependency, no per-token billing.

#Checking availability first

The model only runs on Apple Intelligence–eligible devices with the feature enabled, in a supported region, so every integration starts with an availability check rather than assuming the model exists.

import FoundationModels

let model = SystemLanguageModel.default

switch model.availability {
case .available:
    break
case .unavailable(let reason):
    print("Model unavailable: \(reason)")
}

Branch your UI on this rather than force-using the model. Devices without Apple Intelligence, or with it turned off, are not an edge case you can ignore.

#Sessions hold the conversation

A LanguageModelSession is the object you talk to — it holds prompt and response history across calls, similar to a chat thread, and takes optional instructions (a system prompt) at creation.

let session = LanguageModelSession {
    "You are a concise assistant for a travel app. Keep answers under two sentences."
}

let response = try await session.respond(to: "Suggest a 3-day itinerary theme for Lisbon.")
print(response.content)

Create one session per conversation or task, not one per request — reusing a session across turns is what gives you multi-turn context.

#Guided generation: typed output instead of parsed strings

The framework's standout feature is guided generation: mark a Swift type @Generable and the model returns that type directly, populated and schema-validated, instead of a string you regex apart. @Guide annotations narrow individual fields further.

@Generable
struct TripSuggestion {
    let destination: String

    @Guide(description: "A short, upbeat tagline under ten words")
    let tagline: String

    @Guide(.anyOf(["budget", "moderate", "luxury"]))
    let budgetTier: String
}

let suggestion = try await session.respond(
    to: "Suggest a European city for a first-time solo traveler.",
    generating: TripSuggestion.self
)
print(suggestion.content.destination)

Use this for anything you'd otherwise parse out of free text — categorization, extracting structured fields, generating form-fillable content — rather than prompting for JSON and decoding it yourself.

#Streaming partial results

For longer generations, a streaming variant yields partial values as the model produces them instead of making the caller wait for the full response.

let stream = session.streamResponse(generating: TripSuggestion.self) {
    "Suggest a European city for a first-time solo traveler."
}

for try await partial in stream {
    updateUI(with: partial)
}

Reach for this on anything long enough that a blank screen followed by a sudden full response would feel broken — multi-paragraph text especially.

#Tool calling

A session can call into your app's own code mid-generation through the Tool protocol: declare a name, a description, a @Generable arguments type, and an async call(arguments:) method that returns a ToolOutput. Pass tools in when you create the session, and the model decides when to invoke them based on the prompt and each tool's description.

This is the mechanism for grounding the model in live data it wasn't trained on — a local database lookup, the current date, in-app state — rather than trying to stuff everything into the prompt text.

#Handling errors and guardrails

Generation can fail for reasons specific to this framework, not just network errors: a request can trip the built-in content guardrails, exceed the model's context window, or hit an unsupported language. Wrap calls in do/catch and branch on the specific failure rather than showing one generic error for all of them.

do {
    let response = try await session.respond(to: userPrompt)
    print(response.content)
} catch {
    print("Generation failed: \(error)")
}

The framework's error type distinguishes these cases — worth inspecting rather than swallowing, since a guardrail rejection and a context-window overflow call for different UI responses. Exact case names have shifted across betas, so check the current definition rather than trusting an older blog post's list verbatim.

#What to adopt first

  • Ship the availability check everywhere before anything else — this isn't optional plumbing, it's the majority of your integration work on devices without Apple Intelligence.
  • Start with guided generation on a narrow, low-stakes task — categorizing or summarizing existing in-app text, generating short copy — before reaching for open-ended chat. It plays to what the on-device model is actually good at.
  • Don't design a feature that assumes every user has this model. Apple Intelligence availability, plus devices below the deployment target, is a meaningful chunk of any real install base.
  • Treat tool calling as the way to keep the model honest about app-specific facts, not prompt engineering — for anything the model can't know from training data, give it a tool rather than hoping it doesn't hallucinate.