Using Claude with Apple Foundation Models

•9 min read•Loading views
Using Claude with Apple Foundation Models

App by the author

Nardy — Backgammon & Long Narde

Long Narde and classic backgammon for iPhone. Play a friend or a chatty AI, and on iPhone Duo the fold becomes the bar.

At WWDC26, Apple extended the Foundation Models framework with support for server-side language models. The idea is simple: the same LanguageModelSession API that drives the on-device model can now drive any remote model that conforms to the LanguageModel protocol. Anthropic was quick to adopt it and released ClaudeForFoundationModels — a Swift package that makes Claude a drop-in model for your sessions. Streaming, guided generation with @Generable, and tool calling work the same way.

In this post, we'll build GiftGenie — a small app that generates gift ideas — and let the user switch between the on-device model and Claude with their own API key.

Requirements

  • Xcode 27;
  • iOS 27 (macOS, visionOS, and watchOS 27 are supported as well);
  • A Claude API key from Claude Console for development — see Authentication for production options.

Requests go directly from the app to the Claude API — Apple is not in the request path and doesn't see prompts or responses. Usage is billed to your Anthropic account at standard API pricing.

Adding the package

Add the package via File > Add Package Dependencies… or directly in Package.swift:

dependencies: [
  .package(url: "https://github.com/anthropics/ClaudeForFoundationModels.git", from: "0.1.0")
]

First request

ClaudeLanguageModel is the entry point. Pass it to LanguageModelSession and use the session as usual:

import FoundationModels
import ClaudeForFoundationModels
 
let model = ClaudeLanguageModel(
    name: .sonnet5,
    auth: .apiKey("YOUR_API_KEY")
)
 
let session = LanguageModelSession(model: model)
let response = try await session.respond(to: "Suggest a gift for a sci-fi fan.")
print(response.content)

That's the whole integration. Everything else — instructions, transcripts, structured output — works like with SystemLanguageModel.

Authentication

GiftGenie is a BYOK app: the user pastes their own key, it goes into the Keychain, and requests are billed to them. Nothing secret ships in the binary. If you want to pay for all users with your own key, don't bundle it — a key in an app bundle is extractable from the shipping binary, and whoever pulls it out spends your credits. The package has three auth modes for that reason:

  • .apiKey("...") — development, or BYOK where the user supplies the key. Never ship your own key this way.
  • .proxied(headers:) + baseURL — your backend attaches the credential and forwards plain Messages API requests; the app ships no key. The headers go on every request so the proxy can authorize the caller — pass [:] if it needs none.
  • .appAttest(clientID:) — production without a backend. Each install proves it's a genuine, unmodified build of your app via App Attest, Anthropic issues it a short-lived token, and usage is billed to your workspace.

App Attest setup is three steps: add the App Attest capability under Signing & Capabilities, create an app integration in Claude Console with your Apple Developer Team ID and bundle IDs, and pass the clid_... client ID to ClaudeLanguageModel:

let model = ClaudeLanguageModel(
    name: .sonnet5,
    auth: .appAttest(clientID: "clid_...")
)

The challenge, the attestation, and the token exchange all run inside the package, and tokens are refreshed as they expire — there's no attestation code to write. Tokens live an hour, are scoped to the workspace, and authorize Messages API calls only. They identify the app, not the person using it, so per-user limits stay your job. If a build leaks, revoke the integration and its devices stop getting tokens.

App Attest needs a Secure Enclave, so keep .apiKey for the Simulator. It also works only when the app calls the Claude API directly — not through Amazon Bedrock, Google Cloud, or Microsoft Foundry.

The full reference is Authentication in the package docs.

Generating gift ideas

GiftGenie collects a few details about the recipient and asks the model for structured suggestions. The @Generable macro works with Claude out of the box:

@Generable
struct GiftIdea: Equatable {
    @Guide(description: "Short, catchy gift name")
    var name: String
    @Guide(description: "Why this gift fits the recipient")
    var reasoning: String
    @Guide(description: "Estimated price range, e.g. $20–40")
    var estimatedPrice: String
    @Guide(description: "Category, e.g. Tech, Books, Experience")
    var category: String
}
 
@Generable
struct GiftIdeas: Equatable {
    @Guide(description: "Gift ideas ordered by relevance", .count(5))
    var ideas: [GiftIdea]
}

Under the hood, the package uses structured outputs, so the response is constrained to the generated schema. If you pick a model without structured output support, the package throws LanguageModelError.unsupportedGenerationGuide instead of silently degrading.

Where do the recipient details come from? The app's main screen is a form where the user describes who the gift is for. Its fields are collected into a plain struct:

struct GiftRequest {
    var relationship: String = ""
    var age: Int = 30
    var interests: String = ""
    var occasion: String = ""
    var budget: String = ""
}

The form stays deliberately small — five fields are enough to build focused instructions without turning the UI into prompt engineering. Throughout this post I'm looking for a surprise gift for my wife who likes hiking and knitting, with a $50 budget:

GiftGenie form filled with a gift request for a wife who likes hiking and knitting

Dynamic instructions and profiles

Both SystemLanguageModel and ClaudeLanguageModel conform to the LanguageModel protocol, so models are interchangeable — the session doesn't care which one it gets. GiftGenie stores the user's choice in an observable AppSettings object backed by UserDefaults and Keychain; check the example project for the implementation. But how do we hand the selected model to the session? iOS 27 brings one more API that fits perfectly here — dynamic profiles. By default a session evaluates instructions once at initialization, and they stay static. With dynamic profiles, the framework re-evaluates instructions, tools, and model configuration before every model request, so the session always sees a snapshot of the current app state.

Start with DynamicInstructions — a declarative type whose body builds instructions from the gift request:

struct GiftInstructions: DynamicInstructions {
 
    var request: GiftRequest
 
    var body: some DynamicInstructions {
        Instructions {
            "You are a thoughtful gift advisor. Suggest specific, creative gifts."
            "Recipient: \(request.relationship), age \(request.age)."
            "Interests: \(request.interests)."
            "Occasion: \(request.occasion)."
            "Budget: \(request.budget)."
        }
    }
}

Next, DynamicProfile selects which profile is active. A profile associates instructions with session-level configuration via modifiers, and .model() accepts any LanguageModel — including ClaudeLanguageModel:

struct GiftProfile: LanguageModelSession.DynamicProfile {
 
    var settings: AppSettings
    var request: GiftRequest
 
    var body: some LanguageModelSession.DynamicProfile {
        switch settings.modelChoice {
        case .onDevice:
            Profile {
                GiftInstructions(request: request)
            }
        case .claude:
            Profile {
                GiftInstructions(request: request)
            }
            .model(ClaudeLanguageModel(
                name: settings.claudeModel,
                auth: .apiKey(settings.apiKey)
            ))
        }
    }
}

Profiles support more modifiers like .temperature(), .reasoningLevel(), and .maximumResponseTokens(), lifecycle hooks like onToolCall for approval gates, and historyTransform — a neat trick for multi-model apps: compress the history for the small on-device context window and send the full history to Claude.

When should you escalate to Claude? Apple's on-device model is fast, private, and works offline, but it's sized for lightweight tasks and limited by a small context window — I covered measuring it in Tracking token usage in Foundation Models. Claude brings larger context, frontier reasoning, and server-side tools. With dynamic profiles, switching is one branch in GiftProfile.

Streaming partial content

Frontier models take longer to respond than the on-device one, so streaming is a must for good UX. streamResponse(to:generating:) returns cumulative snapshots of partially generated content, and the UI updates as new gift ideas arrive:

struct GiftResultsView: View {
 
    let request: GiftRequest
 
    @Environment(AppSettings.self) private var settings
    @State private var giftIdeas: GiftIdeas.PartiallyGenerated?
    @State private var errorMessage: String?
 
    var body: some View {
        List {
            ForEach(giftIdeas?.ideas ?? []) { idea in
                VStack(alignment: .leading, spacing: 8) {
                    Text(idea.name ?? "")
                        .font(.headline)
                    Text(idea.reasoning ?? "")
                }
            }
        }
        .task {
            await generate()
        }
    }
 
    private func generate() async {
        do {
            let session = LanguageModelSession(
                profile: GiftProfile(settings: settings, request: request)
            )
            let stream = session.streamResponse(to: "Suggest gift ideas for the recipient.",
                                                generating: GiftIdeas.self)
            for try await partial in stream {
                giftIdeas = partial.content
            }
        } catch {
            print(error.localizedDescription)
        }
    }
}

Every field of PartiallyGenerated is optional, so each row renders what has already arrived. The full version in the example project shows all fields, fades them in with opacity transitions, and covers the wait before the first snapshot with a progress overlay. If you want to polish the streaming UI without burning tokens, check out my post about working with partially generated content in Xcode Previews.

Let's check the result — every row is mapped from the GiftIdea schema:

GiftGenie generated ideas screen with Claude suggestions for hiking and knitting gifts

Error handling

The package maps Claude API errors onto Apple's LanguageModelError where one fits: context-window overflow surfaces as .contextSizeExceeded, HTTP 429 as .rateLimited, and request timeouts as .timeout. Provider-specific errors surface as ClaudeError:

do {
    let response = try await session.respond(to: prompt)
} catch ClaudeError.missingCredential {
    // Prompt for an API key
} catch let error as LanguageModelError {
    // Rate limits, guardrails, context length
} catch {
    // Transport errors
}

Bonus: server-side tools

Claude can use server-side tools — web search, web fetch, and code execution — that run on Anthropic's infrastructure within a single round trip. They are configured on the model rather than on the session, because the session type belongs to Apple:

let model = ClaudeLanguageModel(
    name: .sonnet5,
    auth: auth,
    serverTools: [
        .webSearch(maxUses: 5),
        .codeExecution,
    ]
)

With web search enabled, GiftGenie could check actual prices or find trending gifts.

Conclusion

Foundation Models is becoming a universal interface for language models on Apple platforms: one session API, one @Generable macro, and swappable models — from on-device to frontier. The integration takes minutes, and dynamic profiles make model selection declarative — your users choose between privacy and power, the session picks it up on the next request.

If you like this approach but can't require iOS 27, check out AnyLanguageModel from Hugging Face — a drop-in replacement for the Foundation Models framework that works back to iOS 17 and supports many backends behind the same session API: MLX, llama.cpp, Ollama, Core ML, and remote providers like Anthropic, OpenAI, and Gemini.

I created GiftGenieExample with all the code from this post, so you can run it in Xcode right away. Happy gifting!

Resources