はじめに
こんにちは、システム開発部のT.Nです。
前回の記事、SwiftUIとUIKitで Foundation Models を使ってみた:オンデバイス要約の最小実装 では、Foundation Models の基本的な使い方を最小構成で紹介しました。
今回はその続編として、Foundation Models の @Generable を使い、生成結果をただの文字列ではなく Swift の型(struct)として受け取るところまで踏み込んでみます。
題材として「料理名を入力すると、材料リスト(必要なら簡単なレシピ)を出力する」ミニアプリを作ります。
文字列で返すだけでも動きますが、材料をUIで一覧表示したり、材料の分量を扱ったりすることを考えると、任意の型で扱えることがかなり効いてきます。この記事では、まず「料理名 → 材料リスト」を型で受け取り、UIに表示してみることをゴールにします。
今回やること(完成イメージ)
今回作るのは、料理名を入力すると材料リストを生成して表示するシンプルなSwiftUIアプリです。
まずは前回の延長として、Foundation Models に問い合わせて 文字列として材料を返すところから始めます。ここまでなら実装は簡単ですが、返ってくる内容が文章だとUI側で扱いづらく、材料を一覧表示しにくいという課題があります。
そこで次に、Foundation Models の @Generable を使って、生成結果を Swiftの型(struct)として受け取り、材料を List で表示できる形に作り替えます。これにより、UI側での整形や拡張がしやすくなります。
最後に発展編として、任意で レシピ手順も同じく型として追加し、「材料+手順」まで返せるようにしてみます。
⚠️ 注意:Foundation Models は iOS 26+(iPadOS/macOS/visionOS も同様)かつ Apple Intelligence 有効化が前提です。
詳細は前回の記事をご参照ください。
文字列で材料を返す
最初に、前回の記事と同じ流れで 「モデルに問い合わせて、結果を文字列として受け取る」 ところまでを作ります。
この段階の目的は、とにかく 最小構成で動かすことです。UIは「入力欄+送信ボタン+結果表示」だけに絞ります。
コード全体
import SwiftUI
import FoundationModels
struct ContentView: View {
@State private var text: String = ""
@State private var result: String = ""
@State private var isLoading: Bool = false
let model = SystemLanguageModel.default
private func send(text: String) async throws {
let instructions = """
あなたは料理の材料を教えてくれるアシスタントです。
ユーザーが入力した料理に必要な材料を返してください。
"""
let session = LanguageModelSession(model: model, instructions: instructions)
let prompt = """
ユーザーの入力は\(text)です。
この料理を作るのに必要な材料を教えてください。
"""
isLoading = true
let response = try await session.respond(to: prompt)
isLoading = false
self.result = response.content
}
var body: some View {
if model.isAvailable {
VStack {
Text("""
こんにちは!
今から作りたい料理を教えてください!
材料を教えます!
""")
.padding()
.fixedSize(horizontal: false, vertical: true)
TextField("作りたいものはなんですか?", text: $text)
.textFieldStyle(.roundedBorder)
.padding()
Button(action: {
Task {
do {
try await send(text: text)
} catch {
result = "エラーが発生しました: \(error.localizedDescription)"
}
}
}) {
Text("送る")
}
.buttonStyle(.borderedProminent)
.tint(.black)
if isLoading {
ProgressView()
} else {
ScrollView {
Text(result)
.padding()
}
}
}
} else {
Text("Not available")
}
}
}
出来上がったアプリ

実装としては LanguageModelSession を作り、respond(to:) を呼び出して返ってきた response.content(文字列)を画面に表示するだけです。
ただし、この方法だと返ってくるのはあくまで文章なので、例えば「材料を1件ずつ List に表示したい」「量だけ取り出したい」といった用途では、あとからパースや整形が必要になります。
次のセクションでは、この課題を @Generable で解決していきます。
@Generable を導入して、材料をListで扱える形にする
ここからは、Foundation Models の @Generable を使って、生成結果を「文章」ではなくSwift の型として受け取るようにします。
材料のクラスを @Generable で定義する
まず、モデルに生成してほしい材料の形を @Generable で定義します。今回は「材料名」と「目安の量」だけに絞ってシンプルにしました。
@Generable
public struct Ingredient {
@Guide(description: "材料の名前")
let name: String
@Guide(description: "どのくらい購入すればいいかの目安(例: 200g / 2個)")
let amount: String
}
生成結果を「Ingredient配列」として受け取る
次に、呼び出し側で respond(to:generating:) を使い、生成結果の型として [Ingredient] を指定します。これにより、返ってくる結果は文字列ではなく Ingredient の配列として扱えるようになります。
let response = try await session.respond(to: prompt, generating: [Ingredient].self)
この形にしておくと、SwiftUI 側は ForEach(ingredients, id: \.name) のようにそのままリスト表示できるため、コード上で扱いやすくなりました。
全体のコード
import SwiftUI
import FoundationModels
struct ContentView: View {
@State private var text: String = ""
@State private var result: [Ingredient] = []
@State private var error: String? = nil
@State private var isLoading: Bool = false
let model = SystemLanguageModel.default
private func send(text: String) async {
isLoading = true
defer { isLoading = false }
do {
let instructions = """
あなたは料理の材料を教えてくれるアシスタントです。
ユーザーが入力した料理に必要な材料を返してください。
材料は「材料名」「目安の量」を含めてください。
"""
let session = LanguageModelSession(model: model, instructions: instructions)
let prompt = """
ユーザーの入力は「\(text)」です。
この料理を作るのに必要な材料を教えてください。
"""
let response = try await session.respond(to: prompt, generating: [Ingredient].self)
self.result = response.content
self.error = nil
} catch {
self.error = "エラーが発生しました: \(error.localizedDescription)"
}
}
var body: some View {
if model.isAvailable {
VStack {
Text("""
こんにちは!
今から作りたい料理を教えてください!
材料を教えます!
""")
.padding()
.fixedSize(horizontal: false, vertical: true)
TextField("作りたいものはなんですか?", text: $text)
.textFieldStyle(.roundedBorder)
.padding()
Button("送る") {
Task { await send(text: text) }
}
.buttonStyle(.borderedProminent)
.tint(.black)
.disabled(isLoading)
if isLoading {
ProgressView()
} else {
ScrollView {
if let error {
Text(error)
} else {
ForEach(result, id: \.name) { ingredient in
HStack {
Text(ingredient.name)
.padding(.horizontal)
Text(ingredient.amount)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
.padding(.horizontal)
}
}
}
}
} else {
Text("Not available")
}
}
}
@Generable
public struct Ingredient {
@Guide(description: "材料の名前")
let name: String
@Guide(description: "どのくらい購入すればいいかの目安(例: 200g / 2個)")
let amount: String
}
@Generable によって生成結果を型として受け取れるため、UI側はシンプルに実装でき、拡張もしやすい構成になりました。

応用編
@Generableと@Guideを使って材料だけでなく、調理法や難易度なども出力してみたいと思います。
@Guideには以下のように.rangeや.countをつけることによって値を制限することができます。
@Guide(description: "何人前か", .range(1...6))
let servings: Int
これらの機能を使って出力内容の情報を増やしてみます。
@Generable
public struct Recipe {
@Guide(description: "料理名。料理名が読み取れない場合は「料理名を入力してください」。")
let title: String
@Guide(description: "何人前か", .range(1...6))
let servings: Int
@Guide(description: "調理時間(分)", .range(1...180))
let cookTimeMinutes: Int
@Guide(description: "難易度(easy/normal/hard のいずれか)")
let difficulty: Difficulty
@Guide(description: "材料の配列", .count(8))
let ingredients: [Ingredient]
@Guide(description: "手順(短文)", .count(6))
let steps: [String]
}
コード全体はこちら
import SwiftUI
import FoundationModels
struct ContentView: View {
@State private var text: String = ""
@State private var result: Recipe = .init(
title: "",
servings: 2,
cookTimeMinutes: 15,
difficulty: .easy,
ingredients: [],
steps: []
)
@State private var error: String? = nil
@State private var isLoading: Bool = false
let model = SystemLanguageModel.default
private func send(text: String) async {
isLoading = true
defer { isLoading = false }
do {
let instructions = """
あなたは料理の材料と手順を教えてくれるアシスタントです。
ユーザーが入力した料理に必要な材料と手順を返してください。
"""
let session = LanguageModelSession(model: model, instructions: instructions)
let prompt = """
ユーザーの入力は「\(text)」です。
この料理を作るのに必要な材料と手順を教えてください。
"""
let response = try await session.respond(to: prompt, generating: Recipe.self)
self.result = response.content
self.error = nil
} catch {
self.error = "エラーが発生しました: \(error.localizedDescription)"
}
}
var body: some View {
if model.isAvailable {
VStack(spacing: 12) {
Text("""
こんにちは!
今から作りたい料理を教えてください!
材料と手順を教えます!
""")
.padding(.top, 8)
.fixedSize(horizontal: false, vertical: true)
TextField("作りたいものはなんですか?(例: 親子丼)", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("送る") {
Task { await send(text: text) }
}
.buttonStyle(.borderedProminent)
.tint(.black)
.disabled(isLoading)
if isLoading {
ProgressView().padding(.top, 8)
}
if let error {
Text(error).foregroundStyle(.red).padding(.horizontal)
} else {
List {
Section("概要") {
Text(result.title.isEmpty ? "—" : result.title)
HStack {
Text("人数")
Spacer()
Text("\(result.servings)人前").foregroundStyle(.secondary)
}
HStack {
Text("調理時間")
Spacer()
Text("\(result.cookTimeMinutes)分").foregroundStyle(.secondary)
}
HStack {
Text("難易度")
Spacer()
Text(result.difficulty.label).foregroundStyle(.secondary)
}
}
Section("材料") {
if result.ingredients.isEmpty {
Text("—")
.foregroundStyle(.secondary)
} else {
ForEach(result.ingredients, id: \.name) { ingredient in
HStack {
Text(ingredient.name)
Spacer()
Text(ingredient.amount)
.foregroundStyle(.secondary)
}
}
}
}
Section("手順") {
if result.steps.isEmpty {
Text("—")
.foregroundStyle(.secondary)
} else {
ForEach(Array(result.steps.enumerated()), id: \.offset) { idx, step in
HStack(alignment: .top, spacing: 8) {
Text("\(idx + 1).")
.foregroundStyle(.secondary)
Text(step)
}
}
}
}
}
}
}
} else {
Text("Not available")
}
}
}
@Generable
public struct Recipe {
@Guide(description: "料理名。料理名が読み取れない場合は「料理名を入力してください」。")
let title: String
@Guide(description: "何人前か", .range(1...6))
let servings: Int
@Guide(description: "調理時間(分)", .range(1...180))
let cookTimeMinutes: Int
@Guide(description: "難易度(easy/normal/hard のいずれか)")
let difficulty: Difficulty
@Guide(description: "材料の配列", .count(8))
let ingredients: [Ingredient]
@Guide(description: "手順(短文)", .count(6))
let steps: [String]
}
@Generable
public struct Ingredient {
@Guide(description: "材料の名前")
let name: String
@Guide(description: "どのくらい購入すればいいかの目安(例: 200g / 2個)")
let amount: String
}
@Generable
public enum Difficulty: String, CaseIterable {
case easy
case normal
case hard
var label: String {
switch self {
case .easy: return "かんたん"
case .normal: return "ふつう"
case .hard: return "むずかしい"
}
}
}
完成したアプリ

少し調理手順が怪しい(カレーソースの作り方が斬新)ですが、かなり機能が充実しましたね!
終わりに
今回は、Foundation Models の @Generable を使って、生成結果を文字列ではなく Swift の型(struct)として受け取る実装を試してみました。
まずは respond(to:) で文字列として材料を返すところから始め、次に respond(to: generating:) で [Ingredient] を直接受け取るようにすることで、SwiftUI 側は ForEach でそのまま表示でき、後処理のパースが不要になりました。
さらに応用編では、Recipe(材料+手順+メタ情報)に拡張し、@Guide(.range) や @Guide(.count)、enum を組み合わせることで、出力の揺れを抑えながら情報量を増やすことができました。
生成結果の品質(今回だと調理手順の妥当性)は、プロンプトや @Guide の設計でまだ改善の余地がありますが、「生成AIの結果をアプリのデータとして扱える」という体験はかなり強力でした。
次のステップとしては、材料のカテゴリ分け(野菜/肉/調味料)を型で固定して表示を整える、ユーザー条件(人数・アレルギー・時短など)を入力として追加して精度を上げる、といった方向に拡張していくと実用度がさらに上がりそうです。
ここまで読んでいただき、ありがとうございました。
