Foundation Models Code-Along Instructions
Overview
Note: These instructions and sample code are associated with the Meet with Apple event: Code along with the Foundation Models framework
In this code-along session, you’ll gain practical experience using the Foundation Models framework to integrate Apple’s on-device large language model (LLM) into an application. You’ll build generative AI features into a sample app directly within Xcode, exploring core capabilities such as text generation, structured data output, streaming, and tool calling.
This guide is designed to be used in conjunction with the live coding demonstration. Each chapter provides Playground snippets for isolated experimentation, App steps for extending the starter Xcode project, and Model definitions for custom types and tools. As the instructor demonstrates, use the playground code to explore the APIs, then implement the App steps to integrate these features into SwiftUI views and view models. By the end of this session, you’ll have a fully functional app and a solid understanding of how to incorporate Foundation Models into your own projects.
Prerequisites
Complete the following steps to prepare your environment for the code-along.
1. Download the Xcode project
Begin by downloading the necessary project files.
- Download the project zip file
- After downloading, unzip the file. You’ll find the folder named
FoundationModelsCodeAlong
. This is the starter project for this code-along. It contains the necessary views, models, and placeholder code.
2. Configure Code Signing
Next, open the starter project and configure code signing.
- Open the
FoundationModelsCodeAlong
project in Xcode. - In the Project Navigator, select the project file (the top-level blue icon).
- Click on the
FoundationModelsCodeAlong
target. - Navigate to the Signing & Capabilities tab.
- From the “Team” dropdown menu, select your developer team.
3. Build and Run the App (⌘+R)
Verify that your setup is correct by building and running the app.
- Select My Mac as the run destination in the Xcode toolbar.
- Press Command-R (⌘+R) to build and run the app.
The sample app should launch, displaying a list of featured landmarks. This is the starting point for adding generative features during the session.
Chapter 1: Foundation Models framework basics
This chapter introduces the fundamentals of using the Foundation Models framework to generate text. You’ll explore basic text generation, guiding the model with instructions, and handling model availability.
Playground: Generating content and handling availability
1.1: Making your first generation request
This playground demonstrates the basic process of generating text from a simple prompt.
Copy this code into Playground.swift
:
import FoundationModels
import Playgrounds
#Playground {
// Create a new session with the language model.
let session = LanguageModelSession()
// Asynchronously generate a response from a text prompt.
let response = try await session.respond(to: "Generate a 3-day itinerary to Paris.")
}
1.2: Guiding the model with instructions
While a simple prompt can be effective, using instructions allows you to define a persona, set rules, and specify the desired format. This results in more consistent and higher-quality output. This section demonstrates how to initialize a LanguageModelSession
with instructions that guide its behavior for all subsequent prompts within that session.
Add this new playground block to your file:
#Playground {
let instructions = """
Your job is to create an itinerary for the user.
Each day needs an activity, hotel and restaurant.
Always include a title, a short description, and a day-by-day plan.
"""
let session = LanguageModelSession(instructions: instructions)
let response = try await session.respond(to: "Generate a 3-day itinerary to Paris.")
}
1.3: Handling model availability
Your app may run on devices where the on-device model is unavailable. It’s important to check for model availability and provide a graceful fallback.
Add this playground block to your file:
#Playground {
let model = SystemLanguageModel.default
// The availability property provides detailed information on the model's state.
switch model.availability {
case .available:
print("Foundation Models is available and ready to go!")
case .unavailable(.deviceNotEligible):
print("The model is not available on this device.")
case .unavailable(.appleIntelligenceNotEnabled):
print("Apple Intelligence is not enabled in Settings.")
case .unavailable(.modelNotReady):
print("The model is not ready yet. Please try again later.")
case .unavailable(let other):
print("The model is unavailable for an unknown reason.")
}
}
App: Building the foundation and seeing first results
Apply the core concepts you’ve learned by adding an availability check, creating a ViewModel
to manage the session, and calling the model to display the raw text output in your app.
1.4: Handle availability in the view
Open Views/1-LandmarkDetailView.swift
. Replace the placeholder Text
view with a check for model availability.
-
Next, inside the
LandmarkDetailView
struct, create an instance of the system’s default language model.// MARK: - [CODE-ALONG] Chapter 1.4.1: Add a model instance private let model = SystemLanguageModel.default
-
Delete the placeholder availability enum.
// MARK: - [CODE-ALONG] Chapter 1.4.2: Remove placeholder availability private enum Availability { case available, unavailable } private let availability: Availability = .available
-
Replace the
switch
statementavailability
withmodel.availability
. Replace:// MARK: - [CODE-ALONG] Chapter 1.4.3: Replace availability with model.availability switch availability
With:
switch model.availability
Show the updated Views/1-LandmarkDetailView.swift
import FoundationModels
import SwiftUI
struct LandmarkDetailView: View {
let landmark: Landmark
private let model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
LandmarkTripView(landmark: landmark)
case .unavailable(.appleIntelligenceNotEnabled):
MessageView(
landmark: self.landmark,
message: """
Trip Planner is unavailable because \
Apple Intelligence has not been turned on.
"""
)
default:
MessageView(
landmark: self.landmark,
message: """
Trip Planner is unavailable. Try again later.
"""
)
}
}
}
1.5: Create the itinerary generator
Open ViewModels/ItineraryGenerator.swift
. This file already contains some starter code.
-
Inside the class, add a private property to hold the
LanguageModelSession
.// MARK: - [CODE-ALONG] Chapter 1.5.1: Add a session property private var session: LanguageModelSession
-
Update the initializer by adding instructions for the model and initializing the
LanguageModelSession
.// MARK: - [CODE-ALONG] Chapter 1.5.2: Initialize LanguageModelSession let instructions = """ Your job is to create an itinerary for the user. Each day needs an activity, hotel and restaurant. Always include a title, a short description, and a day-by-day plan. """ self.session = LanguageModelSession(instructions: instructions)
-
Update the placeholder asynchronous
generateItinerary
function so it can be invoked to generate itineraries and store the result initineraryContent
.// MARK: - [CODE-ALONG] Chapter 1.5.3: Add itinerary generator using Foundation Models do { let prompt = "Generate a \(dayCount)-day itinerary to \(landmark.name)." let response = try await session.respond(to: prompt) self.itineraryContent = response.content } catch { self.error = error }
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
// MARK: - [CODE-ALONG] Chapter 2.3.1: Update to Generable
// MARK: - [CODE-ALONG] Chapter 4.1.1: Change the property to hold a partially generated Itinerary
private(set) var itineraryContent: String?
// MARK: - [CODE-ALONG] Chapter 5.3.1: Add a property to hold the tool
init(landmark: Landmark) {
self.landmark = landmark
let instructions = """
Your job is to create an itinerary for the user.
Each day needs an activity, hotel and restaurant.
Always include a title, a short description, and a day-by-day plan.
"""
self.session = LanguageModelSession(instructions: instructions)
// MARK: - [CODE-ALONG] Chapter 5.3.2: Initialize LanguageModelSession with Tool
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = "Generate a \(dayCount)-day itinerary to \(landmark.name)."
let response = try await session.respond(to: prompt)
self.itineraryContent = response.content
} catch {
self.error = error
}
// MARK: - [CODE-ALONG] Chapter 2.3.2: Update to use Generables
// MARK: - [CODE-ALONG] Chapter 3.3: Update to use one-shot prompting
// MARK: - [CODE-ALONG] Chapter 4.1.2: Update to use streaming API
// MARK: - [CODE-ALONG] Chapter 5.3.1: Update the instructions to use the Tool
// MARK: - [CODE-ALONG] Chapter 5.3.2: Update the LanguageModelSession with the tool
// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema in prompt
}
func prewarmModel() {
// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model
}
}
1.6: Update the view to display text output
Open Views/2-LandmarkTripView.swift
. You’ll connect this view to the new ItineraryGenerator
.
-
Declare a local property that holds an instance of
ItineraryGenerator
.// MARK: - [CODE-ALONG] Chapter 1.6.1: Add a local variable of type `ItineraryGenerator` @State private var itineraryGenerator: ItineraryGenerator?
-
Create an instance of the
ItineraryGenerator
when the view appears using the.task
modifier.// MARK: - [CODE-ALONG] Chapter 1.6.2: Create the generator when the view appears let generator = ItineraryGenerator(landmark: landmark) self.itineraryGenerator = generator
-
Replace the
EmptyView()
in theScrollView
else {}
block with logic to display the generated content.// MARK: - [CODE-ALONG] Chapter 1.6.3: Replace EmptyView with model output else if let content = itineraryGenerator?.itineraryContent { Text(LocalizedStringKey(content)) .padding() }
-
Make the “Generate Itinerary” button appear by commenting out the
.hidden()
view modifier.ItineraryButton { requestedItinerary = true // MARK: - [CODE-ALONG] Chapter 1.6.4: Generate itinerary await itineraryGenerator?.generateItinerary() } // MARK: - [CODE-ALONG] Chapter 1.6.4: Show the button //.hidden()
Show the updated Views/2-LandmarkTripView.swift
import SwiftUI
struct LandmarkTripView: View {
let landmark: Landmark
@State private var itineraryGenerator: ItineraryGenerator?
@State private var requestedItinerary: Bool = false
var body: some View {
ScrollView {
if !requestedItinerary {
VStack(alignment: .leading, spacing: 16) {
Text(landmark.name)
.padding(.top, 150)
.font(.largeTitle)
.fontWeight(.bold)
Text(landmark.shortDescription)
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - [CODE-ALONG] Chapter 2.4: Update the Text view with `ItineraryView`
else if let content = itineraryGenerator?.itineraryContent {
Text(LocalizedStringKey(content))
.padding()
}
}
.scrollDisabled(!requestedItinerary)
.safeAreaInset(edge: .bottom) {
ItineraryButton {
requestedItinerary = true
await itineraryGenerator?.generateItinerary()
}
}
.task {
let generator = ItineraryGenerator(landmark: landmark)
self.itineraryGenerator = generator
// MARK: - [CODE-ALONG] Chapter 6.1.2: Pre-warm the model when the view appears
}
.headerStyle(landmark: landmark)
}
}
Build and Run the App (⌘+R): You now have a working end-to-end feature! Tapping “Generate Itinerary” displays a loading indicator and then displays the generated text.
Chapter 2: Generating structured outputs
This chapter explores how to generate structured data, such as Swift structs, using the Foundation Models framework. This enables type-safe and predictable results for your generative AI features.
Playground: Getting structured data
2.1: Generating simple structured output
Generating raw text from the model can be useful, but getting structured data like a Swift struct
unlocks powerful possibilities. This allows for type-safe, predictable results.
This section demonstrates how to use the @Generable
macro to define a simple Swift type that the language model can generate.
Copy this block into Playground.swift
:
import FoundationModels
import Playgrounds
#Playground {
let instructions = """
Your job is to create an itinerary for the user.
"""
let session = LanguageModelSession(instructions: instructions)
let prompt = "Generate a 3-day itinerary to Paris."
let response = try await session.respond(to: prompt,
generating: SimpleItinerary.self)
}
// The @Generable macro makes your custom type compatible with the model.
@Generable
struct SimpleItinerary {
// The @Guide macro provides hints to the model about a property.
@Guide(description: "An exciting name for the trip.")
let title: String
@Guide(description: "A short, engaging description of the trip.")
let description: String
@Guide(description: "A list of day-by-day plans, as simple strings.")
@Guide(.count(3))
let days: [String]
}
2.2: Generating nested structured output
You can create complex data structures by nesting @Generable
types. This allows you to define a rich data model that the language model can populate.
Learn how to define and generate complex, nested Swift types by composing multiple @Generable
structs and enums.
Add this block to Playground.swift
:
#Playground {
let instructions = """
Your job is to create an itinerary for the user.
"""
let session = LanguageModelSession(instructions: instructions)
let prompt = "Generate a 3-day itinerary to the Grand Canyon."
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
}
App: Modeling and displaying the structured itinerary
Refactor the app to use structured outputs. Define the app’s Itinerary
model and update the ViewModel
and View
to generate and display it.
2.3: Refactoring the itinerary generator
Open ViewModels/ItineraryGenerator.swift
. You’ll update it to generate our new Itinerary
struct instead of a raw String
.
- Change the type of
itineraryContent
to our newItinerary
struct and rename it toitinerary
.// MARK: - [CODE-ALONG] Chapter 2.3.1: Update to Generable private(set) var itinerary: Itinerary?
- Update the
generateItinerary
function to request theItinerary.self
type from the model.// MARK: - [CODE-ALONG] Chapter 2.3.2: Update to use Generables let response = try await session.respond(to: prompt, generating: Itinerary.self) self.itinerary = response.content
- Simplify instructions by removing structural guidance
// MARK: - [CODE-ALONG] Chapter 2.3.3: Update instructions to remove structural guidance let instructions = """ Your job is to create an itinerary for the user. Each day needs an activity, hotel and restaurant. """
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
// MARK: - [CODE-ALONG] Chapter 4.1.1: Change the property to hold a partially generated Itinerary
private(set) var itinerary: Itinerary?
// MARK: - [CODE-ALONG] Chapter 5.3.1: Add a property to hold the tool
init(landmark: Landmark) {
self.landmark = landmark
let instructions = """
Your job is to create an itinerary for the user.
Each day needs an activity, hotel and restaurant.
"""
self.session = LanguageModelSession(instructions: instructions)
// MARK: - [CODE-ALONG] Chapter 5.3.2: Initialize LanguageModelSession with Tool
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = "Generate a \(dayCount)-day itinerary to \(landmark.name)."
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
self.itinerary = response.content
} catch {
self.error = error
}
// MARK: - [CODE-ALONG] Chapter 3.3: Update to use one-shot prompting
// MARK: - [CODE-ALONG] Chapter 4.1.2: Update to use streaming API
// MARK: - [CODE-ALONG] Chapter 5.3.1: Update the instructions to use the Tool
// MARK: - [CODE-ALONG] Chapter 5.3.2: Update the LanguageModelSession with the tool
// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema in prompt
}
func prewarmModel() {
// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model
}
}
2.4: Updating the views to display the structured data
The starter project includes a view in Views/3-ItineraryView.swift
that renders the structured output from the model.
Open Views/2-LandmarkTripView.swift
. You’ll update LandmarkTripView
to render this view instead of the text string output.
Update itineraryContent
to itinerary
and replace Text()
with a call to ItineraryView
.
// MARK: - [CODE-ALONG] Chapter 2.4: Update the Text view with `ItineraryView`
else if let itinerary = itineraryGenerator?.itinerary {
ItineraryView(landmark: landmark, itinerary: itinerary).padding()
}
Show the updated Views/2-LandmarkTripView.swift
import SwiftUI
struct LandmarkTripView: View {
let landmark: Landmark
@State private var itineraryGenerator: ItineraryGenerator?
@State private var requestedItinerary: Bool = false
var body: some View {
ScrollView {
if !requestedItinerary {
VStack(alignment: .leading, spacing: 16) {
Text(landmark.name)
.padding(.top, 150)
.font(.largeTitle)
.fontWeight(.bold)
Text(landmark.shortDescription)
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
}
else if let itinerary = itineraryGenerator?.itinerary {
ItineraryView(landmark: landmark, itinerary: itinerary).padding()
}
}
.scrollDisabled(!requestedItinerary)
.safeAreaInset(edge: .bottom) {
ItineraryButton {
requestedItinerary = true
await itineraryGenerator?.generateItinerary()
}
}
.task {
let generator = ItineraryGenerator(landmark: landmark)
self.itineraryGenerator = generator
// MARK: - [CODE-ALONG] Chapter 6.1.2: Pre-warm the model when the view appears
}
.headerStyle(landmark: landmark)
}
}
Build and Run the App (⌘+R): The app now generates a full Itinerary
object and displays it in a formatted, custom view.
Chapter 3: Prompting techniques
This chapter explores techniques to improve the accuracy and consistency of the model’s output using advanced prompting strategies.
Playground: Improving accuracy with one-shot or few-shot prompting
3.1: Building prompts with PromptBuilder
As prompts become more complex, the @PromptBuilder
allows you to dynamically construct your prompt using Swift syntax like if
statements and loops.
Add this block to Playground.swift
:
import FoundationModels
import Playgrounds
#Playground {
let instructions = "Your job is to create an itinerary for the user."
let session = LanguageModelSession(instructions: instructions)
let kidFriendly = true
// The Prompt builder allows for conditional logic.
let prompt = Prompt {
"Generate a 3-day itinerary to the Grand Canyon."
if kidFriendly {
"The itinerary must be kid-friendly."
}
}
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
}
3.2: One-shot prompting
You can significantly improve the quality and reliability of the model’s output by providing one or more high-quality examples of the desired output directly in the prompt. This technique is known as “few-shot prompting.” Learn how to construct a Prompt
that includes both your request and a complete example.
Add this block to Playground.swift
:
#Playground {
let instructions = "Your job is to create an itinerary for the user."
let session = LanguageModelSession(instructions: instructions)
let kidFriendly = false
// Use the Prompt builder to combine your request with an example.
let prompt = Prompt {
"Generate a 3-day itinerary to the Grand Canyon."
if kidFriendly {
"The itinerary must be kid-friendly."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
}
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
}
App: Integrating the few-shot prompt
3.3: Update the itinerary generator with the example
Open ViewModels/ItineraryGenerator.swift
and modify the generateItinerary
function to use the Prompt {...}
builder syntax and include our new example.
Update the generateItinerary
function to include one-shot prompting.
// MARK: - [CODE-ALONG] Chapter 3.3: Update to use one-shot prompting
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
// MARK: - [CODE-ALONG] Chapter 4.1.1: Change the property to hold a partially generated Itinerary
private(set) var itinerary: Itinerary?
// MARK: - [CODE-ALONG] Chapter 5.3.1: Add a property to hold the tool
init(landmark: Landmark) {
self.landmark = landmark
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"For each day, you must suggest one hotel and one restaurant."
}
self.session = LanguageModelSession(instructions: instructions)
// MARK: - [CODE-ALONG] Chapter 5.3.2: Initialize LanguageModelSession with Tool
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
self.itinerary = response.content
} catch {
self.error = error
}
// MARK: - [CODE-ALONG] Chapter 3.3: Update to use one-shot prompting
// MARK: - [CODE-ALONG] Chapter 4.1.2: Update to use streaming API
// MARK: - [CODE-ALONG] Chapter 5.3.1: Update the instructions to use the Tool
// MARK: - [CODE-ALONG] Chapter 5.3.2: Update the LanguageModelSession with the tool
// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema in prompt
}
func prewarmModel() {
// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model
}
}
Build and Run the App (⌘+R): The app’s functionality will appear the same, but the quality and consistency of the generated itineraries should be significantly improved.
Chapter 4: Streaming responses
This chapter demonstrates how to refactor the app to use the streaming API, providing a more engaging user experience by displaying the itinerary as it’s being generated.
App: Building a streaming UI
This section focuses on implementing a streaming UI, skipping the playground examples as streaming is best demonstrated within the context of a full application.
4.1: Update the itinerary generator for streaming
Open ViewModels/ItineraryGenerator.swift
. You’ll modify the generation function to use the streaming API.
-
Update the
itinerary
property. Its type needs to beItinerary.PartiallyGenerated?
to support streaming data.// MARK: - [CODE-ALONG] Chapter 4.1.1: Change the property to hold a partially generated Itinerary private(set) var itinerary: Itinerary.PartiallyGenerated?
-
Update the
generateItinerary
function. Usesession.streamResponse
and loop over the results, updating ouritinerary
property with each new partial result.Replace:
// MARK: - [CODE-ALONG] Chapter 4.1.2: Update to use streaming API let response = try await session.respond(to: prompt, generating: Itinerary.self) self.itinerary = response.content
With:
let stream = session.streamResponse(to: prompt, generating: Itinerary.self) for try await partialResponse in stream { self.itinerary = partialResponse.content }
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
private(set) var itinerary: Itinerary.PartiallyGenerated?
// MARK: - [CODE-ALONG] Chapter 5.3.1: Add a property to hold the tool
init(landmark: Landmark) {
self.landmark = landmark
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"For each day, you must suggest one hotel and one restaurant."
}
self.session = LanguageModelSession(instructions: instructions)
// MARK: - [CODE-ALONG] Chapter 5.3.2: Initialize LanguageModelSession with Tool
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self)
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
// MARK: - [CODE-ALONG] Chapter 5.3.1: Update the instructions to use the Tool
// MARK: - [CODE-ALONG] Chapter 5.3.2: Update the LanguageModelSession with the tool
// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema in prompt
}
func prewarmModel() {
// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model
}
}
4.2: Updating views to render streaming content
First, open Views/3-ItineraryView.swift
. You’ll update it to handle PartiallyGenerated
content.
-
Update generables with
PartiallyGenerated
.Replace
let itinerary: Itinerary ... let plan: DayPlan ... let activities: [Activity] ...
With:
let itinerary: Itinerary.PartiallyGenerated ... let plan: DayPlan.PartiallyGenerated ... let activities: [Activity].PartiallyGenerated
-
Unwrap the
Itinerary
,DayPlan
, andActivity
properties. For example, to accessitinerary.title
, unwrap it usingif let
.Replace
// MARK: - [CODE-ALONG] Chapter 4.2.2: Update to unwrap all PartiallyGenerated types Text(itinerary.title) .contentTransition(.opacity) .font(.largeTitle) .fontWeight(.bold)
With:
if let title = itinerary.title { Text(title) .contentTransition(.opacity) .font(.largeTitle) .fontWeight(.bold) }
-
Repeat this for all properties:
Itinerary
:title
,description
,rationale
DayPlan
:title
,subtitle
,destination
Activity
:title
,description
Show the updated Views/3-ItineraryView.swift
import FoundationModels
import SwiftUI
import MapKit
struct ItineraryView: View {
let landmark: Landmark
let itinerary: Itinerary.PartiallyGenerated
var body: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading) {
if let title = itinerary.title {
Text(title)
.contentTransition(.opacity)
.font(.largeTitle)
.fontWeight(.bold)
}
if let description = itinerary.description {
Text(description)
.contentTransition(.opacity)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
HStack(alignment: .top) {
Image(systemName: "sparkles")
if let rationale = itinerary.rationale {
Text(rationale)
.contentTransition(.opacity)
.rationaleStyle()
}
}
if let days = itinerary.days {
ForEach(days, id: \.title) { plan in
DayView(
landmark: landmark,
plan: plan
)
}
}
}
.animation(.easeOut, value: itinerary)
.itineraryStyle()
}
}
private struct DayView: View {
let landmark: Landmark
let plan: DayPlan.PartiallyGenerated
@State private var mapItem: MKMapItem?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ZStack(alignment: .bottom) {
LandmarkDetailMapView(
landmark: landmark,
landmarkMapItem: mapItem
)
.task(id: plan.destination) {
guard let destination = plan.destination, !destination.isEmpty else { return }
if let fetchedItem = await LocationLookup().mapItem(atLocation: destination) {
self.mapItem = fetchedItem
}
}
VStack(alignment: .leading) {
if let title = plan.title {
Text(title)
.contentTransition(.opacity)
.font(.headline)
}
if let subtitle = plan.subtitle {
Text(subtitle)
.contentTransition(.opacity)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.blurredBackground()
}
.clipShape(RoundedRectangle(cornerRadius: 12))
.frame(maxWidth: .infinity)
.frame(height: 200)
.padding([.horizontal, .top], 4)
ActivityList(activities: plan.activities ?? [])
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
.padding(.bottom)
.geometryGroup()
.card()
.animation(.easeInOut, value: plan)
}
}
private struct ActivityList: View {
let activities: [Activity].PartiallyGenerated
var body: some View {
ForEach(activities) { activity in
HStack(alignment: .top, spacing: 12) {
if let title = activity.title {
ActivityIcon(symbolName: activity.type?.symbolName)
VStack(alignment: .leading) {
Text(title)
.contentTransition(.opacity)
.font(.headline)
if let description = activity.description {
Text(description)
.contentTransition(.opacity)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
Build and Run the App (⌘+R): This provides a more dynamic user experience. When you tap the button, you’ll see a planning screen, and then the full, detailed itinerary will appear piece-by-piece.
Chapter 5: Tool calling
This chapter demonstrates how to extend the capabilities of the language model by providing it with Tools
. A tool is a Swift function that the model can call to get information or perform an action.
Model: Building a tool
5.1: Building the FindPointsOfInterestTool
You can give the language model new capabilities by providing it with Tools
. A tool is a Swift function that the model can decide to call to get information or perform an action, such as looking up real-time data from an API. Let’s build a tool that can find real points of interest.
Open ViewModels/FindPointsOfInterestTool.swift
. You’ll implement the tool step-by-step.
-
Structs that conform to the
Tool
protocol must have aname
and adescription
. The language model uses thename
to call the tool and thedescription
to understand what the tool does and when to use it.// MARK: - [CODE-ALONG] Chapter 5.1.1: Define tool name and description let name = "findPointsOfInterest" let description = "Finds points of interest for a landmark."
-
Define the different types of places our tool can search for. A Swift
enum
marked with the@Generable
macro is perfect for this, as it provides a list of valid, typed options for the model to choose from.// MARK: - [CODE-ALONG] Chapter 5.1.2: Define searchable categories @Generable enum Category: String, CaseIterable { case hotel case restaurant }
-
Define the tool’s
Arguments
struct. It defines the inputs the model needs to provide when it calls our tool. Mark it as@Generable
and use the@Guide
macro to give the model hints about each property.// MARK: - [CODE-ALONG] Chapter 5.1.3: Define tool arguments @Guide(description: "This is the type of business to look up for.") let pointOfInterest: Category
-
Implement the tool’s call logic. The
call(arguments:)
function contains the core logic of the tool. It receives the arguments from the model, performs an action, and must return aString
result. You’ll call a helper function to get mock data and format it into a user-friendly sentence.// MARK: - [CODE-ALONG] Chapter 5.1.4: Implement the tool's call logic let results = await getSuggestions(category: arguments.pointOfInterest, landmark: landmark.name) return """ There are these \(arguments.pointOfInterest) in \(landmark.name): \(results.joined(separator: ", ")) """
-
Implement the
getSuggestions()
function that provides our data. For this code-along, use aswitch
statement to return a hardcoded list of suggestions. In a real-world app, this is where you might make a call to MapKit APIs or other server-side APIs backed by a database.// MARK: - [CODE-ALONG] Chapter 5.1.5: Provide mock data for suggestions switch category { case .hotel : ["Hotel 1", "Hotel 2", "Hotel 3"] case .restaurant : ["Restaurant 1", "Restaurant 2", "Restaurant 3"] }
Show the updated ViewModels/FindPointsOfInterestTool.swift
import FoundationModels
import SwiftUI
@Observable
final class FindPointsOfInterestTool: Tool {
let name = "findPointsOfInterest"
let description = "Finds points of interest for a landmark."
let landmark: Landmark
init(landmark: Landmark) {
self.landmark = landmark
}
@Generable
struct Arguments {
@Guide(description: "This is the type of business to look up for.")
let pointOfInterest: Category
}
func call(arguments: Arguments) async throws -> String {
let results = await getSuggestions(category: arguments.pointOfInterest,
landmark: landmark.name)
return """
There are these \(arguments.pointOfInterest) in \(landmark.name):
\(results.joined(separator: ", "))
"""
}
}
@Generable
enum Category: String, CaseIterable {
case hotel
case restaurant
}
func getSuggestions(category: Category, landmark: String) -> [String] {
switch category {
case .hotel : ["Hotel 1", "Hotel 2", "Hotel 3"]
case .restaurant : ["Restaurant 1", "Restaurant 2", "Restaurant 3"]
}
}
Playground: Extending the model with tools
5.2: Give the model access to the FindPointsOfInterestTool
Switch over to our playground and test the tool.
Add this block to Playground.swift
:
import FoundationModels
import Playgrounds
#Playground {
let landmark = ModelData.landmarks[0]
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"For each day, you must suggest one hotel and one restaurant."
"Always use the 'findPointsOfInterest' tool to find hotels and restaurant in \(landmark.name)"
}
let session = LanguageModelSession(
tools: [pointOfInterestTool],
instructions: instructions
)
let prompt = Prompt {
"Generate a 3-day itinerary to \(landmark.name)."
"Give it a fun title and description."
}
let response = try await session.respond(to: prompt,
generating: Itinerary.self,
options: GenerationOptions(sampling: .greedy))
let inspectSession = session
}
App: Integrating the tool
You’ll introduce the FindPointsOfInterestTool
tool to give ItineraryGenerator
the ability to find points of interest.
5.3: Updating the itinerary generator to use the tool
Open ViewModels/ItineraryGenerator.swift
. You’ll modify the init
method to create an instance of the tool and add it to the LanguageModelSession
.
- In the
init
method, create an instance of theFindPointsOfInterestTool
tool and update the instructions to use it.// MARK: - [CODE-ALONG] Chapter 5.3.1: Update the instructions to use the Tool let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark) let instructions = Instructions { "Your job is to create an itinerary for the user." "For each day, you must suggest one hotel and one restaurant." "Always use the 'findPointsOfInterest' tool to find hotels and restaurant in \(landmark.name)" }
- Pass the tool to the
LanguageModelSession
.// MARK: - [CODE-ALONG] Chapter 5.3.2: Update the LanguageModelSession with the tool self.session = LanguageModelSession( tools: [pointOfInterestTool], instructions: instructions )
- Update
session.streamResponse
to include greedy sampling// MARK: - [CODE-ALONG] Chapter 5.3.3: Update `session.streamResponse` to include greedy sampling options: GenerationOptions(sampling: .greedy)
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
private(set) var itinerary: Itinerary.PartiallyGenerated?
init(landmark: Landmark) {
self.landmark = landmark
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"For each day, you must suggest one hotel and one restaurant."
"Always use the 'findPointsOfInterest' tool to find hotels and restaurant in \(landmark.name)"
}
self.session = LanguageModelSession(tools: [pointOfInterestTool], instructions: instructions)
}
func generateItinerary(dayCount: Int = 3) async {
// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema from prompt
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self,
options: GenerationOptions(sampling: .greedy))
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
}
func prewarmModel() {
// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model
}
}
Build and Run the App (⌘+R): The generated itinerary will now include the specific names of places returned by the tool, like “Restaurant 1” and “Hotel 2”.
Chapter 6: Performance and optimization
This chapter explores key techniques to improve the performance of generative features: pre-warming the model and optimizing the prompt.
App: Improving app performance
6.1: Pre-warming the model
Pre-warming loads the model into memory before it’s needed, reducing the latency of the first generation request.
-
Add a
prewarm
function: OpenViewModels/ItineraryGenerator.swift
and add this new function to the class:// MARK: - [CODE-ALONG] Chapter 6.1.1: Add a function to pre-warm the model session.prewarm()
-
Call
prewarm
when the view appears: OpenViews/2-LandmarkTripView.swift
and add a call to our new function inside the.task
modifier:// MARK: - [CODE-ALONG] Chapter 6.1.2: Pre-warm the model when the view appears generator.prewarmModel()
6.2: Optimizing the prompt
When you provide a good few-shot example, the model often doesn’t need to see the full schema definition. You can remove it to save space and speed up processing.
- Update the
streamResponse
call: Go back toViewModels/ItineraryGenerator.swift
and find thegenerateItinerary
function. - Modify the
session.streamResponse
call to add theincludeSchemaInPrompt: false
parameter:// MARK: - [CODE-ALONG] Chapter 6.2.1: Update to exclude schema in prompt includeSchemaInPrompt: false
Show the updated ViewModels/ItineraryGenerator.swift
import Foundation
import FoundationModels
import Observation
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
private(set) var itinerary: Itinerary.PartiallyGenerated?
init(landmark: Landmark) {
self.landmark = landmark
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"Each day needs an activity, hotel and restaurant."
"""
Always use the findPointsOfInterest tool to find businesses
and activities in \(landmark.name), especially hotels and restaurants.
The point of interest categories may include hotel and restaurant.
"""
landmark.description
}
self.session = LanguageModelSession(tools: [pointOfInterestTool],
instructions: instructions)
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self,
includeSchemaInPrompt: false)
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
}
func prewarmModel() {
session.prewarm()
}
}
Show the updated Views/2-LandmarkTripView.swift
import SwiftUI
struct LandmarkTripView: View {
let landmark: Landmark
@State private var itineraryGenerator: ItineraryGenerator?
@State private var requestedItinerary: Bool = false
var body: some View {
ScrollView {
if !requestedItinerary {
VStack(alignment: .leading, spacing: 16) {
Text(landmark.name)
.padding(.top, 150)
.font(.largeTitle)
.fontWeight(.bold)
Text(landmark.shortDescription)
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
}
else if let itinerary = itineraryGenerator?.itinerary {
ItineraryView(landmark: landmark, itinerary: itinerary).padding()
}
}
.scrollDisabled(!requestedItinerary)
.safeAreaInset(edge: .bottom) {
ItineraryButton {
requestedItinerary = true
await itineraryGenerator?.generateItinerary()
}
}
.task {
let generator = ItineraryGenerator(landmark: landmark)
self.itineraryGenerator = generator
generator.prewarmModel()
}
.headerStyle(landmark: landmark)
}
}
Build and Run the App (⌘+R): These optimizations reduce the “time to first token” and make the generative feature feel more responsive, especially on the first run.
Recap and next steps
Congratulations! You’ve completed the Foundation Models framework code-along. You’ve learned how to generate content, guide generation with structured types, build a responsive UI with streaming, extend the model’s capabilities with tools, and optimize performance.
Here’s a quick recap of the key concepts we covered:
- Chapter 1: Foundation Models framework basics: We started with the fundamentals of making a generation request, using instructions to guide the model’s output, and checking for model availability to ensure a smooth user experience.
- Chapter 2: Generating structured outputs: We moved beyond simple text generation to receiving structured data from the model. Using the
@Generable
macro, we defined custom Swift types that the model can populate, allowing for more predictable and type-safe results. - Chapter 3: Prompting techniques: We explored advanced prompting techniques to improve the accuracy and reliability of the model’s output. We learned how to use the prompt builder to construct dynamic prompts and how to provide one-shot examples to guide the model.
- Chapter 4: Streaming responses: To enhance the user experience, we implemented streaming to display the model’s response as it’s being generated. This involved updating our
ViewModel
to handle partially generated content and modifying our views to render the streaming data. - Chapter 5: Tool calling: We extended the model’s capabilities by creating a
Tool
. This allowed the model to call our Swift code to get information and perform actions, such as finding points of interest. - Chapter 6: Performance and optimization: Finally, we optimized our app’s performance by pre-warming the model to reduce latency and by optimizing the prompt to save space and speed up processing.
We’re excited to see what you will build next!