Swift Foundation Models: Chat
Apple's Foundation Models Framework provides simple access to language models. The sample below shows, how to use a `LanguageModelSession` to send a message and react to a stream of responses.
The feature has been tested on a MacBook with Silicon processor, macOS 26 Tahoe and Xcode 26.
The communication with the LanguageModelSession is very simple:
import SwiftUI
import Foundation
import FoundationModels
@available(iOS 26.0, *)
struct SampleChatView: View {
class Message: ObservableObject, Identifiable {
let id: UUID = UUID()
let timestamp: Date = Date()
@Published var text: String
let isUser: Bool
@Published var isUpdating: Bool = false
init(text: String, isUser: Bool) {
self.text = text
self.isUser = isUser
}
}
@MainActor
class Model: ObservableObject {
@Published var messageText: String = ""
@Published var messages: [Message] = []
@Published var isBusy: Bool = false
@Published var updateTick: Bool = false
private var languageModelSession: LanguageModelSession?
init() {
languageModelSession = LanguageModelSession()
}
func send(message: Message) async {
let responseMessage = Message(text: "", isUser: false)
messages.append(responseMessage)
updateTick.toggle()
do {
guard let session = languageModelSession else { return }
responseMessage.isUpdating = true
let stream = session.streamResponse(to: message.text)
isBusy = true
messageText = ""
for try await response in stream {
responseMessage.text = response
updateTick.toggle()
}
responseMessage.isUpdating = false
isBusy = false
} catch {
print("Error sending message: \(String(describing: error))")
}
}
}
struct MessageView: View {
@ObservedObject var message: Message
var body: some View {
HStack {
if message.isUser {
Spacer()
}
Text(message.text.isEmpty ? " ... " : message.text)
.padding()
.background {
RoundedRectangle(cornerRadius: 16)
.fill(message.isUser ? Color(white: 0.2) : Color(white: 0.4))
}
.overlay(alignment: .bottomTrailing) {
message.isUpdating ? ProgressView()
.padding(8)
: nil
}
if !message.isUser {
Spacer()
}
}
.foregroundStyle(Color.white)
}
}
@StateObject private var model: Model = Model()
var body: some View {
VStack(alignment: .leading, spacing: 15) {
Text("AI Assistant")
.font(.title)
.foregroundStyle(Color.orange)
.padding(.horizontal, 12)
ScrollViewReader { svr in
List {
ForEach(model.messages) { msg in
MessageView(message: msg)
.id(msg.id)
}
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.background(Color(white: 0.1))
.listStyle(.plain)
.onChange(of: model.updateTick) { _ in
if let m = model.messages.last {
withAnimation {
svr.scrollTo(m.id, anchor: .bottom)
}
}
}
}
TextField("", text: $model.messageText, prompt: Text("Your message ...").foregroundStyle(Color.orange), axis: .vertical)
.padding(12)
.background(Color(.systemGray6))
.foregroundColor(Color(white: 0.7))
.clipShape(RoundedRectangle(cornerRadius: 16))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.orange, lineWidth: 1)
)
.padding(.horizontal, 12)
HStack {
Spacer()
Button(action: {
send()
}, label: {
Text("Send")
.foregroundStyle(model.isBusy ? Color.gray : Color.orange)
.padding(10)
})
.buttonStyle(.bordered)
.disabled(model.isBusy)
}
.padding(.horizontal, 12)
}
.background(Color(white: 0.15))
.preferredColorScheme(.dark)
}
private func send() {
let userMessage = Message(text: model.messageText, isUser: true)
model.messages.append(userMessage)
Task {
await model.send(message: userMessage)
}
}
}
@available(iOS 26.0, *)
#Preview {
SampleChatView()
}