From ecc
iOS 26+에서 Apple FoundationModels 프레임워크를 사용해 온디바이스 LLM을 다루는 패턴입니다. 텍스트 생성, `@Generable` 기반 가이드 생성, tool calling, snapshot streaming을 다룹니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
FoundationModels 프레임워크를 사용해 Apple의 온디바이스 언어 모델을 앱에 통합하는 패턴입니다. 텍스트 생성, `@Generable` 기반 구조화 출력, 커스텀 tool calling, snapshot streaming을 다룹니다. 모두 디바이스 내에서 실행되므로 개인정보 보호와 오프라인 지원에 적합합니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
FoundationModels 프레임워크를 사용해 Apple의 온디바이스 언어 모델을 앱에 통합하는 패턴입니다. 텍스트 생성, @Generable 기반 구조화 출력, 커스텀 tool calling, snapshot streaming을 다룹니다. 모두 디바이스 내에서 실행되므로 개인정보 보호와 오프라인 지원에 적합합니다.
세션을 만들기 전에 항상 모델 사용 가능 여부를 확인합니다.
struct GenerativeView: View {
private var model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
ContentView()
case .unavailable(.deviceNotEligible):
Text("Device not eligible for Apple Intelligence")
case .unavailable(.appleIntelligenceNotEnabled):
Text("Please enable Apple Intelligence in Settings")
case .unavailable(.modelNotReady):
Text("Model is downloading or not ready")
case .unavailable(let other):
Text("Model unavailable: \\(other)")
}
}
}
// Single-turn: create a new session each time
let session = LanguageModelSession()
let response = try await session.respond(to: "What's a good month to visit Paris?")
print(response.content)
// Multi-turn: reuse session for conversation context
let session = LanguageModelSession(instructions: """
You are a cooking assistant.
Provide recipe suggestions based on ingredients.
Keep suggestions brief and practical.
""")
let first = try await session.respond(to: "I have chicken and rice")
let followUp = try await session.respond(to: "What about a vegetarian option?")
instructions 작성 시 핵심:
"You are a mentor""Help extract calendar events""Respond as briefly as possible""I can't help with that"라고 답하기@Generable 기반 가이드 생성원시 문자열 대신 구조화된 Swift 타입을 생성합니다.
@Generable(description: "Basic profile information about a cat")
struct CatProfile {
var name: String
@Guide(description: "The age of the cat", .range(0...20))
var age: Int
@Guide(description: "A one sentence profile about the cat's personality")
var profile: String
}
let response = try await session.respond(
to: "Generate a cute rescue cat",
generating: CatProfile.self
)
// Access structured fields directly
print("Name: \\(response.content.name)")
print("Age: \\(response.content.age)")
print("Profile: \\(response.content.profile)")
@Guide 제약.range(0...20) — 숫자 범위.count(3) — 배열 원소 수description: — 생성 의미론 가이드모델이 도메인 전용 작업을 위해 커스텀 코드를 호출하게 합니다.
struct RecipeSearchTool: Tool {
let name = "recipe_search"
let description = "Search for recipes matching a given term and return a list of results."
@Generable
struct Arguments {
var searchTerm: String
var numberOfResults: Int
}
func call(arguments: Arguments) async throws -> ToolOutput {
let recipes = await searchRecipes(
term: arguments.searchTerm,
limit: arguments.numberOfResults
)
return .string(recipes.map { "- \\($0.name): \\($0.description)" }.joined(separator: "\\n"))
}
}
let session = LanguageModelSession(tools: [RecipeSearchTool()])
let response = try await session.respond(to: "Find me some pasta recipes")
do {
let answer = try await session.respond(to: "Find a recipe for tomato soup.")
} catch let error as LanguageModelSession.ToolCallError {
print(error.tool.name)
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
// Handle specific tool error
}
}
PartiallyGenerated 타입으로 구조화된 응답을 스트리밍해 실시간 UI를 만듭니다.
@Generable
struct TripIdeas {
@Guide(description: "Ideas for upcoming trips")
var ideas: [String]
}
let stream = session.streamResponse(
to: "What are some exciting trip ideas?",
generating: TripIdeas.self
)
for try await partial in stream {
// partial: TripIdeas.PartiallyGenerated (all properties Optional)
print(partial)
}
@State private var partialResult: TripIdeas.PartiallyGenerated?
@State private var errorMessage: String?
var body: some View {
List {
ForEach(partialResult?.ideas ?? [], id: \\.self) { idea in
Text(idea)
}
}
.overlay {
if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
}
.task {
do {
let stream = session.streamResponse(to: prompt, generating: TripIdeas.self)
for try await partial in stream {
partialResult = partial
}
} catch {
errorMessage = error.localizedDescription
}
}
}
| Decision | Rationale |
|---|---|
| 온디바이스 실행 | 데이터가 기기를 떠나지 않아 개인정보 보호와 오프라인 지원에 유리 |
| 4,096 토큰 제한 | 온디바이스 모델 제약. 큰 입력은 여러 세션으로 나눠야 함 |
| Snapshot streaming | 구조화 출력 친화적. 각 snapshot이 완전한 partial 상태 |
@Generable 매크로 | 구조화 생성에 대한 컴파일타임 안정성 제공, PartiallyGenerated 타입 자동 생성 |
| 세션당 한 요청 | isResponding이 동시 요청을 막음. 여러 요청은 세션을 여러 개 생성 |
response.content 사용 | 올바른 API. 결과 접근은 항상 .content |
model.availability 확인 — 세션 생성 전 모든 unavailable 케이스 처리instructions 적극 활용 — 프롬프트보다 우선 적용됩니다isResponding 확인 — 세션은 한 번에 하나의 요청만 처리response.content로 접근 — .output이 아님@Generable 사용 — 원시 문자열 파싱보다 안전GenerationOptions(temperature:)로 창의성 조절model.availability 확인 없이 세션 생성.content 대신 .output 사용@Generable로 충분한데도 원시 문자열을 직접 파싱