Skip to content

Commit 7f5c1b2

Browse files
Add post: Using Claude with Apple Foundation Models
1 parent e098659 commit 7f5c1b2

6 files changed

Lines changed: 258 additions & 1 deletion

File tree

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
---
2+
title: Using Claude with Apple Foundation Models
3+
description: Bringing frontier models to LanguageModelSession with model selection and BYOK
4+
cover: /images/using-claude-with-apple-foundation-models/cover.png
5+
date: '2026-06-12'
6+
categories: foundation-models, ai, llm, claude
7+
author:
8+
- artem-novichkov
9+
---
10+
11+
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](https://github.com/anthropics/ClaudeForFoundationModels) — a Swift package that makes Claude a drop-in model for your sessions. Streaming, guided generation with `@Generable`, and tool calling work exactly the same way.
12+
13+
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.
14+
15+
## Requirements
16+
17+
- Xcode 27 beta;
18+
- iOS 27 beta (macOS, visionOS, and watchOS 27 are supported as well);
19+
- A Claude API key from [Claude Console](https://platform.claude.com).
20+
21+
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.
22+
23+
## Adding the package
24+
25+
Add the package via **File > Add Package Dependencies…** or directly in `Package.swift`:
26+
27+
```swift
28+
dependencies: [
29+
.package(url: "https://github.com/anthropics/ClaudeForFoundationModels.git", from: "0.1.0")
30+
]
31+
```
32+
33+
## First request
34+
35+
`ClaudeLanguageModel` is the entry point. Pass it to `LanguageModelSession` and use the session as usual:
36+
37+
```swift
38+
import FoundationModels
39+
import ClaudeForFoundationModels
40+
41+
let model = ClaudeLanguageModel(
42+
name: .sonnet4_6,
43+
auth: .apiKey("YOUR_API_KEY")
44+
)
45+
46+
let session = LanguageModelSession(model: model)
47+
let response = try await session.respond(to: "Suggest a gift for a sci-fi fan.")
48+
print(response.content)
49+
```
50+
51+
That's the whole integration. Everything else — instructions, transcripts, structured output — works like with `SystemLanguageModel`.
52+
53+
<Callout>
54+
A key bundled into an app is extractable from the shipping binary. Use `.apiKey` for development or BYOK scenarios only. For production, the package provides `.proxied(headers:)` mode that routes requests through your own backend, and Anthropic promises App Attest-based authentication soon.
55+
</Callout>
56+
57+
## Generating gift ideas
58+
59+
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:
60+
61+
```swift
62+
@Generable
63+
struct GiftIdea: Equatable {
64+
@Guide(description: "Short, catchy gift name")
65+
var name: String
66+
@Guide(description: "Why this gift fits the recipient")
67+
var reasoning: String
68+
@Guide(description: "Estimated price range, e.g. $20–40")
69+
var estimatedPrice: String
70+
@Guide(description: "Category, e.g. Tech, Books, Experience")
71+
var category: String
72+
}
73+
74+
@Generable
75+
struct GiftIdeas: Equatable {
76+
@Guide(description: "Gift ideas ordered by relevance", .count(5))
77+
var ideas: [GiftIdea]
78+
}
79+
```
80+
81+
Under the hood the package uses [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), so the response is guaranteed to match the schema. If you pick a model without structured output support, the package throws `LanguageModelError.unsupportedGenerationGuide` instead of silently degrading.
82+
83+
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:
84+
85+
```swift
86+
struct GiftRequest {
87+
var relationship: String = ""
88+
var age: Int = 30
89+
var interests: String = ""
90+
var occasion: String = ""
91+
var budget: String = ""
92+
}
93+
```
94+
95+
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:
96+
97+
<p align="center">
98+
<img src="/images/using-claude-with-apple-foundation-models/gift-form.jpg" alt="GiftGenie form filled with a gift request for a wife who likes hiking and knitting" style={{maxWidth: "50%"}} />
99+
</p>
100+
101+
## Dynamic instructions and profiles
102+
103+
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](https://developer.apple.com/documentation/foundationmodels/composing-dynamic-sessions-with-instructions-and-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.
104+
105+
Start with `DynamicInstructions` — a declarative type whose `body` builds instructions from the gift request:
106+
107+
```swift
108+
struct GiftInstructions: DynamicInstructions {
109+
110+
var request: GiftRequest
111+
112+
var body: some DynamicInstructions {
113+
Instructions {
114+
"You are a thoughtful gift advisor. Suggest specific, creative gifts."
115+
"Recipient: \(request.relationship), age \(request.age)."
116+
"Interests: \(request.interests)."
117+
"Occasion: \(request.occasion)."
118+
"Budget: \(request.budget)."
119+
}
120+
}
121+
}
122+
```
123+
124+
Next, `DynamicProfile` orchestrates which profile is active. A profile associates instructions with session-level configuration via modifiers, and `.model()` accepts any `LanguageModel` — including `ClaudeLanguageModel`:
125+
126+
```swift
127+
struct GiftProfile: LanguageModelSession.DynamicProfile {
128+
129+
var settings: AppSettings
130+
var request: GiftRequest
131+
132+
var body: some LanguageModelSession.DynamicProfile {
133+
switch settings.modelChoice {
134+
case .onDevice:
135+
Profile {
136+
GiftInstructions(request: request)
137+
}
138+
case .claude:
139+
Profile {
140+
GiftInstructions(request: request)
141+
}
142+
.model(ClaudeLanguageModel(
143+
name: settings.claudeModel,
144+
auth: .apiKey(settings.apiKey)
145+
))
146+
}
147+
}
148+
}
149+
```
150+
151+
The result builder enforces at compile time that exactly one profile is active. Because `body` re-evaluates before each request, changing the model in Settings takes effect on the next request — even in a long-lived session. 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.
152+
153+
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 4096-token context window — I covered measuring it in [Tracking token usage in Foundation Models](https://www.artemnovichkov.com/blog/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`.
154+
155+
## Streaming partial content
156+
157+
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:
158+
159+
```swift
160+
struct GiftResultsView: View {
161+
162+
let request: GiftRequest
163+
164+
@Environment(AppSettings.self) private var settings
165+
@State private var giftIdeas: GiftIdeas.PartiallyGenerated?
166+
@State private var errorMessage: String?
167+
168+
var body: some View {
169+
List {
170+
ForEach(giftIdeas?.ideas ?? []) { idea in
171+
VStack(alignment: .leading, spacing: 8) {
172+
Text(idea.name ?? "")
173+
.font(.headline)
174+
Text(idea.reasoning ?? "")
175+
}
176+
}
177+
}
178+
.task {
179+
await generate()
180+
}
181+
}
182+
183+
private func generate() async {
184+
do {
185+
let session = LanguageModelSession(
186+
profile: GiftProfile(settings: settings, request: request)
187+
)
188+
let stream = session.streamResponse(to: "Suggest gift ideas for the recipient.",
189+
generating: GiftIdeas.self)
190+
for try await partial in stream {
191+
giftIdeas = partial.content
192+
}
193+
} catch {
194+
print(error.localizedDescription)
195+
}
196+
}
197+
}
198+
```
199+
200+
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](https://www.artemnovichkov.com/blog/working-with-partially-generated-content-in-xcode-previews).
201+
202+
Let's check the result — every row is mapped from the `GiftIdea` schema:
203+
204+
<p align="center">
205+
<img src="/images/using-claude-with-apple-foundation-models/generated-ideas.jpg" alt="GiftGenie generated ideas screen with Claude suggestions for hiking and knitting gifts" style={{maxWidth: "50%"}} />
206+
</p>
207+
208+
<Callout>
209+
In my testing, streaming didn't work — neither on the Simulator nor on a device: the response arrives only as a final snapshot. I haven't found a solution yet; remember that both the OS betas and the package are in beta. I'll update the article once it's resolved.
210+
</Callout>
211+
212+
## Error handling
213+
214+
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`:
215+
216+
```swift
217+
do {
218+
let response = try await session.respond(to: prompt)
219+
} catch ClaudeError.missingCredential {
220+
// Prompt for an API key
221+
} catch let error as LanguageModelError {
222+
// Rate limits, guardrails, context length
223+
} catch {
224+
// Transport errors
225+
}
226+
```
227+
228+
## Bonus: server-side tools
229+
230+
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:
231+
232+
```swift
233+
let model = ClaudeLanguageModel(
234+
name: .sonnet4_6,
235+
auth: auth,
236+
serverTools: [
237+
.webSearch(maxUses: 5),
238+
.codeExecution,
239+
]
240+
)
241+
```
242+
243+
With web search enabled, GiftGenie could check actual prices or find trending gifts.
244+
245+
## Conclusion
246+
247+
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.
248+
249+
If you like this approach but can't require iOS 27, check out [AnyLanguageModel](https://github.com/huggingface/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.
250+
251+
I created [GiftGenieExample](https://github.com/artemnovichkov/GiftGenieExample) with all the code from this post, so you can run it in Xcode right away. Note that both the OS 27 betas and the package are in beta, so APIs may change before general availability. Happy gifting!
252+
253+
## Resources
254+
255+
- [Composing dynamic sessions with instructions and profiles](https://developer.apple.com/documentation/FoundationModels/composing-dynamic-sessions-with-instructions-and-profiles) — Apple Documentation
256+
- [What's new in the Foundation Models framework](https://developer.apple.com/videos/play/wwdc2026/241/) — WWDC26 session

content/posts/using-model-context-protocol-in-ios-apps.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Using Model Context Protocol in iOS apps
33
description: Learn how to implement MCP client with SwiftUI and Anthropic API
44
cover: /images/using-model-context-protocol-in-ios-apps/cover.png
55
date: "2025-05-11"
6-
categories: mcp, ai, llm, swiftui
6+
categories: mcp, ai, llm, swiftui, claude
77
author:
88
- artem-novichkov
99
---
281 KB
Loading
83.5 KB
Loading
21.1 KB
Loading

src/lib/const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const title: string = `${name} – ${about}`
55
export const categoryTitleMap: Record<string, string> = {
66
ai: "AI",
77
avkit: "AVKit",
8+
claude: "Claude",
89
combine: "Combine",
910
concurrency: "Concurrency",
1011
"core-animation": "Core Animation",

0 commit comments

Comments
 (0)