Foundation Models Frameworkを用いてチャットアプリを実装する
はじめに
iOS 26で導入された Foundation Models Framework
は、Apple の大規模言語モデル(LLM)をオンデバイスで直接利用できる画期的な機能です。
これにより、OpenAIなどの外部のAPIに依存することなく、プライバシーを保護しながらローカルで言語タスクを実行できるようになりました。
本記事では、SwiftUIとFoundation Models Frameworkを用いて、オンデバイスで動作するチャットアプリの実装方法を解説します。
実装の全体像
今回実装するチャットアプリは、以下の主要な要素で構成されます。
- メッセージモデル: チャットのメッセージの構造を定義します。
- ChatViewModel: Foundation Models との連携、メッセージ管理、ストリーミングレスポンスの処理を行います。
- ContentView (SwiftUI): ViewModel のデータを監視しチャットUIを表示・更新、ユーザー操作を処理します。
1. メッセージモデルの定義
チャットメッセージを表現するための基本的なデータ構造を定義します。
各メッセージが一意になるようIdentifiable
に準拠させています。
enum Role {
case user
case assistant
}
struct Message: Identifiable {
let id = UUID()
let role: Role
var content: String
}
Role
列挙型でメッセージの送信者を区別し、Message
構造体で各メッセージの情報を管理します。
content
プロパティをvar
で宣言することで、ストリーミング中にリアルタイムで更新できるようにしています。
2. ChatViewModel の実装
ChatViewModel
は、Foundation Models フレームワークとの連携とチャットの状態管理を担います。
ObservableObject
として定義し、SwiftUIとのデータバインディングができるようにします。
import SwiftUI
import Combine
import FoundationModels
class ChatViewModel: ObservableObject {
@Published var isStreaming: Bool = false
@Published var messages: [Message] = []
private var session: LanguageModelSession? // LLMセッション
private var instructions = "親しみやすい絵文字と分かりやすい言葉遣いで、日本語で会話を楽しく、心地よく進めていきます"
private var currentTask: Task<Void, Never>?
// メッセージをクリア
func clearMessages() {
cancelCurrentTask()
messages.removeAll()
}
// taskをキャンセル
func cancelCurrentTask() {
currentTask?.cancel()
currentTask = nil
finishResponse()
}
// メッセージを送信
func sendMessage(content: String) {
// taskをキャンセル
cancelCurrentTask()
// メッセージを作成
let message = Message(role: .user, content: content)
messages.append(message)
// リクエストを送信
isStreaming = true
currentTask = Task {
do {
// ストリーミングレスポンスを処理
try await handleStreamingResponse(message)
// レスポンスの処理を終了
if !Task.isCancelled {
finishResponse()
}
} catch {
if !Task.isCancelled {
finishResponse()
}
}
}
}
// ストリーミングレスポンスを処理
private func handleStreamingResponse(_ message: Message) async throws {
if self.session == nil {
self.session = LanguageModelSession(instructions: self.instructions)
}
guard let currentSession = self.session else {
throw NSError(domain: "ChatViewModel", code: -1, userInfo: [NSLocalizedDescriptionKey: "LanguageModelSession is not initialized"])
}
let stream = currentSession.streamResponse(to: message.content)
for try await response in stream {
if Task.isCancelled { break }
let content = response.content
await MainActor.run {
// 最後のメッセージがアシスタントの場合は更新、そうでなければ追加
if let lastMessage = messages.last, lastMessage.role == .assistant {
messages[messages.count - 1].content = content
} else {
messages.append(Message(role: .assistant, content: content))
}
}
}
}
// レスポンスの処理を終了
@MainActor
private func finishResponse() {
isStreaming = false
}
}
LanguageModelSession
は Foundation Models フレームワークの中核となるクラスで、AIモデルとの対話セッションを管理します。
instructions
でLLMの振る舞いを指定できます。今回は「日本語での親しみやすい会話」をするよう指定しています。
Foundation Models フレームワークのstreamResponse(to:)
メソッドは、非同期ストリームを返します。この非同期ストリームをfor try await
ループで処理することで、生成されるレスポンスをリアルタイムに受信できます。
ポイント:
- ストリーミング中は既存のアシスタントメッセージを更新し、新しいメッセージの場合は追加します
- UI 更新は
MainActor.run
を使用してメインスレッドで実行します - キャンセル処理により、ユーザーがいつでもレスポンス生成を中断できます
3. ChatUIの実装
ContentView
ContentView
は、チャットUIを構成するViewです。
新しいチャットの開始、メッセージの表示、ユーザー入力の3つの主なセクションに分かれています。
import SwiftUI
struct ContentView: View {
@State private var prompt: String = ""
@StateObject private var chatViewModel = ChatViewModel()
var body: some View {
VStack(spacing: 0) {
// ヘッダー
HStack {
Button(action: {
chatViewModel.clearMessages()
}) {
HStack(spacing: 8) {
Image(systemName: "square.and.pencil")
Text("新しいチャット")
}
}
Spacer()
}
.padding()
// メッセージ
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(chatViewModel.messages) { message in
MessageView(message: message)
.id(message.id)
}
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onChange(of: chatViewModel.messages.last?.content) {
if let lastMessage = chatViewModel.messages.last {
withAnimation {
proxy.scrollTo(lastMessage.id, anchor: .bottom)
}
}
}
}
// 入力フィールドと送信ボタン
HStack(spacing: 8) {
TextField("質問してみましょう", text: $prompt)
Spacer()
Button(action: {
if chatViewModel.isStreaming {
chatViewModel.cancelCurrentTask()
} else {
sendMessageWithText(prompt)
}
}) {
Image(systemName: chatViewModel.isStreaming ? "stop.circle.fill" : "arrow.up.circle.fill")
.resizable()
.frame(width: 32, height: 32)
.opacity((!chatViewModel.isStreaming && prompt.isEmpty) ? 0.5 : 1)
}
.disabled(!chatViewModel.isStreaming && prompt.isEmpty)
}
.padding()
}
}
// メッセージ送信
private func sendMessageWithText(_ text: String) {
let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedText.isEmpty && !chatViewModel.isStreaming else { return }
self.prompt = ""
chatViewModel.sendMessage(content: trimmedText)
}
}
// メッセージ表示用のView
private struct MessageView: View {
let message: Message
var body: some View {
HStack(alignment: .top, spacing: 8) {
switch message.role {
case .user:
Spacer()
Text(message.content)
.padding()
.fixedSize(horizontal: false, vertical: true)
.frame(alignment: .trailing)
.background(Color(.systemGray6))
.cornerRadius(9999)
case .assistant:
Text(message.content)
.fixedSize(horizontal: false, vertical: true)
.frame(alignment: .leading)
}
}
}
}
MessageView
は、ユーザが入力したメッセージとAIからのレスポンスを表示するためのViewです。
UI の特徴:
- ストリーミング中のリアルタイム更新に対応したスクロール処理
- ユーザーメッセージとアシスタントメッセージの視覚的な区別
- ストリーミング状態に応じた送信/停止ボタンの動的切り替え
まとめ
本記事では、iOS 26で導入された Foundation Models フレームワークを使用して、完全にオンデバイスで動作するチャットアプリケーションの実装方法を解説しました。
主なポイント:
-
streamResponse(to:)
メソッドでリアルタイムストリーミングレスポンスを受信 -
for try await
を使用した非同期ストリーム処理 - SwiftUI の
@Published
プロパティを介したリアルタイム UI 更新 -
Task
とcancel()
を使用した非同期処理の制御
Foundation Models フレームワークは、プライバシーやコストを重視しながらLLMをアプリに統合したい開発者にとって、非常に魅力的な選択肢です。
また外部 APIへの依存がないため、ユーザーのデータ保護と高速な応答、オフライン動作、費用の削減など、多くの利点があります。
iOS 26以降をターゲットとしたアプリケーションで ローカルLLMを用いたAI機能を実装する際の参考にしていただければ幸いです。
Appendix
Markdownのサポート
下記のようなライブラリを使用して、アシスタントメッセージが吐き出したMarkdownをよりみやすくすることも可能です。
Tool Callingのサポート
Foundation Models フレームワークは、外部ツールの呼び出し、いわゆるTool Callingもサポートしています。
Tool Callingを使用し、iOSの他のフレームワークや外部のAPIと連携することで、さらに高度なチャットアプリケーションを構築できます。
参考
Discussion