Highlight
Foundation Models 框架让 Swift 开发者用
@Generable注解直接生成自定义结构体,无需手写 JSON 解析。
核心内容
旅行规划这件事,最痛苦的是从一张白纸开始:去哪、玩几天、住哪、吃什么。Naomy 在这个 code-along 里要做的就是把这件事丢给端侧大模型——选一个景点(比如 Joshua Tree),点一下 Generate,模型直接吐出一份带每日活动安排的完整行程。整个过程不联网、不上传任何数据,因为模型已经嵌在 macOS、iPadOS、iOS 和 visionOS 的系统里。
但要把模型接进 SwiftUI app,开发者会卡在几个地方:怎么调 prompt 才高效?模型输出是字符串,怎么变成 Swift 结构体?模型要怎么调用 MapKit 这样的外部数据源?输出延迟怎么压下来?设备没开 Apple Intelligence 怎么办?这一节用 30 分钟把这五件事全部演示了一遍: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)。反复跑 app 改 prompt 一天就过去了。Xcode 更新后的 Playground 给出更快的路径:在任意 Swift 文件里写 #Playground,canvas 会像 SwiftUI Preview 一样实时回显结果。改一个字,模型立刻重新跑一遍。
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用尾随闭包接收 instructions,内部是 result builder,可混合字符串与 Generable 实例;respond(to:generating:)显式指定输出类型为Itinerary.self,框架自动把 schema 注入 prompt;- 把 planner 标成
@Observable,UI 才会随itinerary变化刷新; - Naomy 强调(09:22)planner 应该在
.task里创建,而不是直接作为@State初始值,否则 SwiftUI 会反复构造它带来无谓开销。
Tool 协议接外部数据(11:12)。模型自己决定何时调工具、调几次。一个 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——大堡礁配 marina、Joshua Tree 配 desertAttraction 是模型的世界知识在起作用(13:07);call(arguments:)是 async 方法,搜索 20km 半径内的兴趣点,返回ToolOutput;- 注册工具:在
LanguageModelSession初始化时通过tools:参数传入实例,可以同时在 prompt 里显式提示”use the tool”以提升调用频率(15:30)。
可用性处理(16:37)。设备可能不支持 Apple Intelligence、用户可能没开启、模型可能正在下载。三种状态对应三种 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返回模型状态枚举,开发者按 case 分别给降级 UI;- 测试方法不需要真去关 Apple Intelligence,Xcode scheme 里有 Foundation Models Availability override,可以模拟 Device Not Eligible / Apple Intelligence Not Enabled / Model Not Ready 三种状态(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);- 在视图层用
if let title = partial?.title解包,并配合.contentTransition(.interpolate)加动画,效果就是文字一段段平滑渐入。
性能优化(24:24)。新的 Foundation Models Instrument 拆出三条轨道:Asset Loading(蓝色,模型加载耗时)、Inference(推理)、Tool Calling(紫色),还显示 input token 数。Naomy 用两招把首字延迟压下来:
// 优化 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描述,所以示例必须够全——如果有 Optional 属性,示例需要覆盖该属性有值和 nil 两种情况(28:35)。
核心启发
-
做什么:把端侧 LLM 当成业务字段的”自动生成器”,而非聊天框。 为什么值得做:传统做法是模型出 JSON、客户端解析、字段对不齐就崩溃;Guided Generation 把生成约束交给模型 runtime,类型即合约。 怎么开始:选一个目前用模板硬凑的功能(推荐文案、菜单分类、行程摘要),把目标结构体加
@Generable+@Guide,先在#Playground里跑通再接到 UI。 -
做什么:给关键字段加
.contentTransition(.interpolate)+ 流式生成。 为什么值得做:流式能把”等 8 秒看一坨结果”变成”1 秒看到第一行、边读边出”,主观延迟下降一个数量级。 怎么开始:把respond换成streamResponse,把视图绑到PartiallyGenerated类型,逐字段if let解包,给文本和列表加 transition。 -
做什么:在用户行为节点 prewarm。 为什么值得做:模型可能被系统从内存换出,第一次调用要等加载;prewarm 把这段耗时挪到用户感知不到的地方。 怎么开始:找出”用户表达意图”到”用户真正点击生成”之间的等待间隙(点开详情、开始打字、滑动到目标卡片),在
.task或onChange里调session.prewarm()。 -
做什么:把可用性状态做成一等公民的 UI 分支。 为什么值得做:Apple Intelligence 不是所有设备都有,写”出错了”等于劝退用户;按 unavailable 子状态分别引导,能保住转化率。 怎么开始:在主入口 view 顶层 switch
SystemLanguageModel.default.availability,三种 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