Highlight
Foundation Models フレームワークによって、Swift 開発者は
@Generableアノテーションだけでカスタム構造体を直接生成でき、JSON パースを手書きする必要がなくなります。
主要内容
旅行プランニングで一番つらいのは、白紙の状態から始めることです。どこに行くか、何日滞在するか、どこに泊まるか、何を食べるか。本 code-along で Naomy がやろうとしているのは、こうした作業をオンデバイスの大規模言語モデルに丸投げすることです。観光地を 1 つ選び(例えば Joshua Tree)、Generate ボタンを押すだけで、モデルが日ごとのアクティビティ込みの完全な旅程をそのまま吐き出してくれます。一連の処理はネットワーク接続も不要、データのアップロードもありません。モデルが macOS、iPadOS、iOS、visionOS のシステムに組み込まれているからです。
ただし、このモデルを SwiftUI アプリに組み込もうとすると、開発者はいくつかの壁にぶつかります。prompt をどう書けば効率的に動くのか、モデルの出力は文字列だが Swift の構造体にどう変換するのか、MapKit のような外部データソースをモデルから呼び出すにはどうすればよいのか、出力の遅延をどう抑えるのか、デバイスで Apple Intelligence が無効な場合どうするのか——本セッションは 30 分でこの 5 つを一通りデモします。Xcode Playground で prompt をリアルタイムにイテレーションする方法(02:54)、@Generable と @Guide で構造化データを生成させる方法(04:47)、Tool プロトコルを実装して MapKit と連携する方法(11:43)、ストリーミングレスポンスと PartiallyGenerated の組み合わせ(20:15)、そして新しい Foundation Models Instrument でボトルネックを特定し、prewarm と includeSchemaInPrompt: false で初トークンまでの遅延を体感できないレベルまで圧縮する手法(24:24)です。
詳細
Xcode Playground で prompt をチューニングする(02:54)。アプリを何度も走らせて prompt を直していると、それだけで 1 日が終わってしまいます。アップデート後の Xcode の Playground ではより速いパスが用意されました。任意の Swift ファイルに #Playground と書くだけで、canvas が SwiftUI Preview のようにリアルタイムで結果を返してくれます。1 文字直せば、モデルがすぐに再実行されます。
Guided Generation(04:47)。カスタム構造体を @Generable でマークすれば、モデルがその型のインスタンスを直接生成してくれます。@Guide マクロでは説明文の付与、取り得る値の制限、配列の長さの制限などが可能で、複数の guide を重ねて適用できます。
import FoundationModels
@Generable
struct Itinerary {
@Guide(description: "A catchy title for the trip")
var title: String
var description: String
var rationale: String
@Guide(.count(3))
var days: [DayPlan]
}
@Generable
struct DayPlan {
var title: String
var activities: [String]
}
ポイント:
@Generableアノテーションは構造体に付与するもので、その全プロパティも Generable である必要があります(String、Intなどのプリミティブ型は標準でサポート済み)。@Guide(description:)は説明文を schema に埋め込むもので、そのフィールドに対して追加で prompt を書くのと同等の効果があります。@Guide(.count(3))はdays配列を 3 要素ちょうどに強制します。この種の制約は生成段階で直接効くため、結果を受け取った後に検証する必要はありません。
builder API で Instructions を書く(06:50)。Instructions クロージャには文字列だけでなく、Generable インスタンスを few-shot 例として渡すこともできます。Foundation Models が Generable インスタンスをモデルが読めるテキストへ自動変換します。
@Observable
final class ItineraryPlanner {
let landmark: Landmark
var itinerary: Itinerary?
private let session: LanguageModelSession
init(landmark: Landmark) {
self.landmark = landmark
self.session = LanguageModelSession {
"You are a travel planner. Build a 3-day itinerary."
"Landmark: \(landmark.name)"
"Here is an example itinerary:"
Itinerary.exampleJapanTrip
}
}
func generate() async throws {
let response = try await session.respond(
to: "Generate an itinerary for \(landmark.name).",
generating: Itinerary.self
)
itinerary = response.content
}
}
ポイント:
LanguageModelSessionは trailing closure で instructions を受け取ります。内部は result builder になっていて、文字列と Generable インスタンスを混在させて渡せます。respond(to:generating:)で出力型をItinerary.selfと明示すると、フレームワークが schema を自動的に prompt に注入します。- planner を
@Observableにしておかないと、itineraryの変化で UI が再描画されません。 - Naomy が強調していたのは(09:22)、planner は
.taskの中で生成すべきで、@Stateの初期値として直接渡してはいけないという点です。後者だと SwiftUI が繰り返し planner を構築してしまい、無駄なオーバーヘッドが発生します。
Tool プロトコルで外部データを取得する(11:12)。いつ tool を呼ぶか、何回呼ぶかはモデルが自分で判断します。1 つの Tool には次が必要です。一意な name、description(モデルにいつ使うべきかを伝える)、Arguments 型(Generable であること必須)、そして call メソッドです。
import FoundationModels
import MapKit
struct FindPointsOfInterestTool: Tool {
let name = "findPointsOfInterest"
let description = "Search for points of interest near a landmark, given a category."
let landmark: Landmark
@Generable
enum Category: String {
case restaurant
case marina
case desertAttraction
case hikingTrail
}
@Generable
struct Arguments {
@Guide(description: "Natural language search query, e.g. 'sunset viewpoint'")
var query: String
var category: Category
}
func call(arguments: Arguments) async throws -> ToolOutput {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = arguments.query
request.region = MKCoordinateRegion(
center: landmark.coordinate,
latitudinalMeters: 20_000,
longitudinalMeters: 20_000
)
let response = try await MKLocalSearch(request: request).start()
let names = response.mapItems.prefix(5).map(\.name).compactMap { $0 }
return ToolOutput(names.joined(separator: ", "))
}
}
ポイント:
nameとdescriptionはフレームワークが自動的に instructions に書き込み、モデルはそれを根拠に呼び出すかどうかを判断します。Argumentsは Generable でなければなりません。モデルは schema に従って query と category を生成します。Great Barrier Reef には marina、Joshua Tree には desertAttraction を割り当てるあたりは、モデル自身の世界知識が効いています(13:07)。call(arguments:)は async メソッドで、半径 20km 以内の POI を検索してToolOutputを返します。- tool の登録は
LanguageModelSessionの初期化時にtools:引数でインスタンスを渡します。同時に prompt 内で「use the tool」と明示的に指示すると呼び出し頻度を上げられます(15:30)。
可用性のハンドリング(16:37)。デバイスが Apple Intelligence をサポートしていなかったり、ユーザーが有効化していなかったり、モデルがダウンロード中だったりする可能性があります。3 つの状態にはそれぞれ別の UI を用意します。
struct LandmarkTripView: View {
let landmark: Landmark
private let model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
generateButton
case .unavailable(.appleIntelligenceNotEnabled):
Text("Enable Apple Intelligence in Settings to plan trips.")
case .unavailable(.modelNotReady):
Text("Model is still downloading. Please try again later.")
case .unavailable:
funFactView // デバイス自体が非対応のため、生成ボタンは隠す
}
}
}
ポイント:
SystemLanguageModel.default.availabilityがモデルの状態を表す enum を返すので、開発者は case ごとにフォールバック UI を出し分けます。- 動作確認のために実際に Apple Intelligence をオフにする必要はありません。Xcode の scheme に Foundation Models Availability override が用意されており、Device Not Eligible / Apple Intelligence Not Enabled / Model Not Ready の 3 状態をシミュレートできます(17:27)。
ストリーミング出力(20:15)。respond を streamResponse に置き換えると、返ってくるのは完成された Itinerary ではなく Itinerary.PartiallyGenerated になります——全プロパティが自動的に Optional に変換された型です。
func generateStreaming() async throws {
let stream = session.streamResponse(
to: "Generate an itinerary for \(landmark.name).",
generating: Itinerary.self
)
for try await partial in stream {
partialItinerary = partial // 各フレームはより完成度の高い Itinerary.PartiallyGenerated
}
}
ポイント:
- 戻り値は
AsyncSequenceで、イテレーションのたびに少しずつ完成度の上がったバージョンが手に入ります。最初のフレームには title だけ、次のフレームでは description も追加……というイメージです。 PartiallyGenerated型は自動でIdentifiableに準拠するため、SwiftUI のForEachでそのまま使えて、ID 管理を自前でやる必要はありません(22:53)。- View 層では
if let title = partial?.titleでアンラップし、.contentTransition(.interpolate)を組み合わせれば、テキストがセクションごとに滑らかにフェードインしていく演出になります。
パフォーマンスチューニング(24:24)。新しい Foundation Models Instrument は 3 本のトラックを切り出します。Asset Loading(青、モデルのロード時間)、Inference(推論)、Tool Calling(紫)。さらに input token 数も表示してくれます。Naomy は 2 つの手段で初トークンまでの遅延を圧縮しています。
// 最適化 1: ユーザーが landmark の詳細を開いた時点で予熱する
struct LandmarkDescriptionView: View {
let planner: ItineraryPlanner
var body: some View {
descriptionContent
.task {
planner.session.prewarm()
}
}
}
// 最適化 2: schema の注入をスキップする
let response = try await session.respond(
to: "Generate an itinerary for \(landmark.name).",
generating: Itinerary.self,
options: GenerationOptions(includeSchemaInPrompt: false)
)
ポイント:
prewarm()は事前にモデルをメモリに読み込むためのもので、タイミングはユーザーが明確な意図を示した直後(landmark を開いたときなど)にします。説明文を読み終えて Generate を押す頃には、モデルは既に準備完了しています(26:28)。includeSchemaInPrompt: falseにすると schema 分の token を節約できますが、次の前提条件のいずれかを満たす必要があります。同じ session で既に同じ型のリクエストを発行済みであるか、instructions に完全な例が含まれていることです。- 注意点として、schema の注入を切ると
@Guideの説明が失われるため、example は十分に網羅的でなければいけません。Optional プロパティがある場合は、値が入っているケースと nil のケースの両方を例で押さえておく必要があります(28:35)。
重要ポイント
-
何をやるか: オンデバイス LLM をチャットボックスではなく、業務フィールドの「自動ジェネレーター」として捉える。 なぜやる価値があるか: 従来の方法はモデルに JSON を吐かせ、クライアント側でパースし、フィールドが合わなければクラッシュ……というパターンでした。Guided Generation は生成時の制約をモデルランタイムに委ねるため、型がそのまま契約になります。 どう始めるか: 現状テンプレートで力技で組み立てている機能(おすすめ文言、メニュー分類、行程サマリーなど)を 1 つ選び、ターゲットの構造体に
@Generableと@Guideを付け、まず#Playgroundで動かしてから UI に組み込みます。 -
何をやるか: 重要なフィールドに
.contentTransition(.interpolate)とストリーミング生成を組み合わせる。 なぜやる価値があるか: ストリーミングがあると「8 秒待って結果が一気にドン」が「1 秒で 1 行目が出始め、読みながら続きが出てくる」に変わります。体感遅延は一桁単位で下がります。 どう始めるか:respondをstreamResponseに差し替え、View をPartiallyGenerated型に紐付け、フィールドごとにif letでアンラップし、テキストやリストに transition を付与します。 -
何をやるか: ユーザー行動の節目で prewarm を仕込む。 なぜやる価値があるか: モデルはシステムによってメモリから追い出されることがあり、初回呼び出しではロード待ちが発生します。prewarm はそのコストをユーザーが感じない場所に押し込みます。 どう始めるか: 「ユーザーが意図を表明する」から「実際に生成ボタンを押す」までの待ち時間(詳細を開く、入力を始める、対象カードまでスクロールするなど)を見つけ、
.taskかonChangeの中でsession.prewarm()を呼びます。 -
何をやるか: 可用性ステートを UI のファーストクラスな分岐として扱う。 なぜやる価値があるか: Apple Intelligence は全デバイスにあるわけではないため、「エラーが発生しました」とだけ表示するのはユーザーを追い返すのと同じです。unavailable の各サブステートに合わせて誘導すれば転換率を維持できます。 どう始めるか: 主要な entry view のトップで
SystemLanguageModel.default.availabilityを switch し、3 種類の unavailable それぞれに合った文言を出し分け、Xcode scheme の Foundation Models Availability override で全分岐を実機テストします。 -
何をやるか: prompt が安定したら
includeSchemaInPrompt: falseを有効化する。 なぜやる価値があるか: schema token は隠れたコストです。マルチターン対話で同じ schema を毎回送るのは、遅い上にコンテキストの無駄遣いになります。 どう始めるか: まず instructions に完全な Generable の例(Optional プロパティの両ケースをカバー)を書き、その上でGenerationOptionsで schema 注入を切り、Foundation Models Instrument で input token 数を比較します。
関連 Session
- Meet the Foundation Models framework — フレームワークの入門概要。code-along の前にこちらを先に見ておくと理解が早いです。
- Deep dive into the Foundation Models framework — Guided Generation、tool calling、ストリーミング出力の内部メカニズムの掘り下げ。
- Explore prompt design & safety for on-device foundation models — Prompt 設計と安全ガードレールのベストプラクティス。
- Develop for Shortcuts and Spotlight with App Intents — 生成機能を App Intents にラップして、Shortcuts や Spotlight から直接呼び出せるようにする方法。
- Design interactive snippets — AI の生成結果を snippet 形式でシステムの入口に表示するデザイン。
コメント
GitHub Issues · utterances