
这是一个关于在那座被诅咒的城市------浣熊市(Raccoon City),一名绝望的程序员如何在丧尸围城和暴君追杀的双重压力下,利用 Xcode 预览调试 AI 代码的故事。在 R.P.D. 的备用电源耗尽前,Leon 必须利用残缺的 JSON 碎片,在 Xcode 中调试出丝滑的 AI 响应流。
🌧️ 浣熊市警察局(R.P.D.),深夜。
窗外的暴雨夹杂着远处不知名怪物的嘶吼声,狠狠地拍打在警察局二楼办公室的窗户上。屋内,一台靠备用发电机勉强维持的 Mac 屏幕发着幽幽的蓝光,这是这间屋子里唯一比绝望更亮的东西。坐在屏幕前的并不是普通的幸存者,而是 Leon S. Kennedy,他不仅要面对 T 病毒的威胁,此刻更让他头秃的是------如何在断网且随时可能断电的环境下,调试完这个该死的基于大语言模型(LLM)的 SwiftUI 界面。

"Leon,还没好吗?暴君(Mr. X)的脚步声好像在楼道里响了......" 对讲机里传来 Claire 焦急的声音。
"再给我五分钟,Claire!" Leon 擦了一把额头上的汗(或者是冷汗),盯着 Xcode 26.0 的界面咬牙切齿,"这个 AI 生成内容的预览如果不搞定,我们就没法实时分析那个该死的 G 病毒突变数据!"
在本次生存手册中,您将学到如下内容:
-
- [🌧️ 浣熊市警察局(R.P.D.),深夜。](#🌧️ 浣熊市警察局(R.P.D.),深夜。)
- [🧟♂️ 在 Xcode 预览中处理部分生成内容](#🧟♂️ 在 Xcode 预览中处理部分生成内容)
-
- [🐈 生成内容:末日里的精神慰藉](#🐈 生成内容:末日里的精神慰藉)
- [👁️ 预览调试:在此刻欺骗编译器](#👁️ 预览调试:在此刻欺骗编译器)
-
- [1. 静态数据伪装术](#1. 静态数据伪装术)
- [2. 生化危机级别的 JSON 碎片](#2. 生化危机级别的 JSON 碎片)
- [🚫 遇到的坑(Mr. X 的阻挠)](#🚫 遇到的坑(Mr. X 的阻挠))
- [🎬 动起来:让 UI 像里昂的走位一样丝滑](#🎬 动起来:让 UI 像里昂的走位一样丝滑)
- [🚁 结局:逃出生天](#🚁 结局:逃出生天)
我是 Leon,现在是 2026 年。如果你读到这段代码,说明你要么是 Umbrella 公司的内鬼,要么是想在末日里通过精通 Apple 开发来求生的硬核极客。坐稳了,今天我们不谈怎么爆头丧尸,我们来谈谈如何在 Xcode Previews 中优雅地处理 Partially Generated Content(部分生成内容)。

🧟♂️ 在 Xcode 预览中处理部分生成内容
随着 Foundation Models 框架的横空出世,Apple 给了我们这帮苦逼开发者一套神兵利器,让我们能把 AI 生成的内容直接塞进 SwiftUI 应用里。其中有个功能特别像在丧尸堆里找子弹------它能引导生成特定的 Swift 数据结构。
但在实战中(或者说在逃命途中),我们通常没时间等 AI 把整句话说完。我们需要处理 流式传输(Streaming) 的数据。今天,我就带你在丧尸破门之前,搞定在 Xcode 预览里调试这些"说话说一半"的数据。
ℹ️ 情报更新:
以下代码已在 Xcode 26.0 (17A324) 环境下通过实战测试。由于 T 病毒影响,旧版本可能会导致编译错误或"死机"。

🐈 生成内容:末日里的精神慰藉
为了不让自己疯掉,我决定先不用那些恶心的变异体数据做测试。让我们从 Apple 文档里那个温馨的例子开始------生成一个猫咪档案。毕竟,谁不想在末日撸猫呢?
swift
import FoundationModels
// @Generable 宏就像是给 AI 下达的战术指令
@Generable(description: "Basic profile information about a cat")
struct CatProfile: Equatable {
let name: String
// @Guide 告诉 AI 这个字段该怎么"填空"
@Guide(description: "A one sentence profile about the cat's personality")
let profile: String
// 限制年龄范围,防止 AI 产生幻觉生成出一只 500 岁的猫妖
@Guide(description: "The age of the cat", .range(0...20))
let age: Int
}
接下来,我们需要一个视图来展示这个猫咪档案。Claire 在监视器那头催我,我手速飞快地敲下代码:
swift
import SwiftUI
import FoundationModels
struct ContentView: View {
@State private var session = LanguageModelSession()
// 注意这里:CatProfile.PartiallyGenerated? 是关键
// 它代表数据还在生成中,就像一只丧尸刚把手伸出门缝,身体还没进来
@State private var catProfile: CatProfile.PartiallyGenerated?
var body: some View {
NavigationStack {
List {
if let catProfile {
// 展示猫咪档案
CatProfileView(catProfile: catProfile)
}
}
.navigationTitle("Cat Profile")
.task {
do {
// 请求 AI 生成一只可爱的搜救猫
let stream = session.streamResponse(generating: CatProfile.self) {
"Generate a cute rescue cat"
}
// 这是一个异步流,数据会一点一点蹦出来
for try await catProfile in stream {
self.catProfile = catProfile.content
}
} catch {
print("Error generating cat profile: \(error)")
}
}
}
}
}
既然是流式响应,数据就不会瞬间出现。如果你展开 @Generable 宏生成的代码,你会看到 PartiallyGenerated 这个结构体大概长这样。

它就像是被肢解的 CatProfile,每个属性都是可选的(Optional),因为你永远不知道下一秒 AI 会吐出名字还是年龄。
swift
nonisolated struct PartiallyGenerated: Identifiable, nonisolated FoundationModels.ConvertibleFromGeneratedContent, Equatable {
var id: GenerationID
// 所有属性都变成了 .PartiallyGenerated? 类型
// 这就像薛定谔的猫,你不知道它是否存在
var name: String.PartiallyGenerated?
var profile: String.PartiallyGenerated?
var age: Int.PartiallyGenerated?
nonisolated init(_ content: FoundationModels.GeneratedContent) throws {
self.id = content.id ?? GenerationID()
self.name = try content.value(forProperty: "name")
self.profile = try content.value(forProperty: "profile")
self.age = try content.value(forProperty: "age")
}
}
为了把这个残缺的数据展示出来,我们需要一个专门的视图:
swift
import SwiftUI
import FoundationModels
struct CatProfileView: View {
let catProfile: CatProfile.PartiallyGenerated
var body: some View {
VStack(alignment: .leading) {
// 防御性编程:只有当名字生成出来时才显示
if let name = catProfile.name {
Text(name)
.font(.headline)
}
if let profile = catProfile.profile {
Text(profile)
.font(.subheadline)
}
if let age = catProfile.age {
Text("Age: \(age)")
.font(.caption)
}
}
}
}
此时,我听到门外传来沉重的脚步声------咚、咚、咚。是暴君!他就像那个永远存在的 Bug,压迫感十足。
"Leon!我们需要预览界面!现在!" Claire 大喊。

问题来了:在 Xcode Preview 里,我们怎么调试这种流式生成的中间状态? 等 AI 响应就像等 Umbrella 公司的良心发现一样------太慢了!

👁️ 预览调试:在此刻欺骗编译器
要在 Xcode 预览里看到 UI 布局,我们不能真的去请求 AI 模型,那会卡死预览画布,而且在 R.P.D. 的地下室里信号真的很差。我们需要 Mock(模拟数据)。
1. 静态数据伪装术
任何实现了 Generable 的类型都可以通过调用 asPartiallyGenerated() 摇身一变,伪装成部分生成的内容。这招叫"指鹿为马",或者用我的话说------"给丧尸涂上口红装活人"。
swift
extension CatProfile {
// 创建一个名为 Trisha 的假猫数据
static let mock = CatProfile(name: "Trisha",
profile: "A playful and curious cat who loves to explore her surroundings.",
age: 8)
}
#Preview("Mock", traits: .sizeThatFitsLayout) {
// 将完整数据转换为"部分生成"数据进行预览
CatProfileView(catProfile: CatProfile.mock.asPartiallyGenerated())
}
这样我们就能看到最终的布局效果。

但这还不够,我想看到那个"一点点生成"的过程,比如只有名字没有简介的时候,界面会不会崩得像被舔食者(Licker)抓过的脸一样难看?

2. 生化危机级别的 JSON 碎片
我们可以手动构造一些残缺的 JSON 字符串来模拟流式传输的中间态。这有点像法医拼凑尸体,雖然恶心但有效。
swift
#Preview("GeneratedContent", traits: .sizeThatFitsLayout) {
let jsons = [
// 状态 1:只有一半的名字,仿佛 AI 刚开口说话
#"{"name": "Trisha"#,
// 状态 2:有了名字和简介
#"{"name": "Trisha", "profile": "A playful and curious cat"#,
// 状态 3:完整数据
#"{"name": "Trisha", "profile": "A playful and curious cat", "age": 8"#,
]
VStack(spacing: 8) {
ForEach(jsons, id: \.self) { json in
// 强行解析 JSON,即使它是残缺的(Invalid JSON)
// GeneratedContent 竟然能吃下这些垃圾数据,真是胃口好
let content = try! GeneratedContent(json: json)
CatProfileView(catProfile: try! .init(content))
}
}
}
⚠️ 技术黑话: GeneratedContent 非常强悍,即使 JSON 格式不合法(比如缺了右大括号),它也会尽力解析已有的字段。这在处理流式数据时至关重要,因为网络包随时可能断在半路。

🚫 遇到的坑(Mr. X 的阻挠)
起初,我试图直接使用 CatProfile.PartiallyGenerated(content) 进行初始化:
swift
CatProfileView(catProfile: try! CatProfile.PartiallyGenerated(content))

结果 Xcode 甩给我一个红色的错误,就像暴君当面给了我一拳:
🚫 Cannot convert value of type 'CatProfile.PartiallyGenerated' (aka 'CatProfile') to expected argument type 'CatProfile.PartiallyGenerated'
这简直是废话文学!明明是同一个类型却不能转换?这大概是 Apple 的工程师在写编译器时喝多了 T 病毒原液。不管了,用 .init(content) 就能绕过去,逃命要紧,不求甚解。
🎬 动起来:让 UI 像里昂的走位一样丝滑
预览里显示,如果只有名字,内容会居中。这不行,我们要让它看起来自然点。加上动画,让数据出现时有一种"正在解密 Umbrella 机密文件"的感觉。
swift
import SwiftUI
import FoundationModels
struct CatProfileView: View {
let catProfile: CatProfile.PartiallyGenerated
var body: some View {
VStack(alignment: .leading) {
if let name = catProfile.name {
Text(name)
.font(.headline)
.transition(.opacity) // 渐隐出现
}
// ... (省略其他字段,同上)
}
// 关键点:当 catProfile 发生变化时,应用缓动动画
.animation(.easeInOut, value: catProfile)
}
}

模拟流式响应(重头戏)
为了在预览中完美复刻那种"正在打字"的效果,我写了一个扩展方法。这就像是给你的枪装上了无限子弹的作弊码------我们可以手动控制 JSON 的吐字速度。
swift
import FoundationModels
extension Generable {
// 这个函数模拟流式传输:每隔 delay 时间,多读取 distance 长度的字符
static func streamResponse(from json: String,
offsetBy distance: Int = 4,
delay: Duration = .milliseconds(500)) -> AsyncThrowingStream<Self.PartiallyGenerated, Error> {
AsyncThrowingStream { continuation in
Task {
var index = json.startIndex
while index < json.endIndex {
// 每次多切一片肉(字符串)下来
let nextIndex = json.index(index, offsetBy: distance, limitedBy: json.endIndex) ?? json.endIndex
let substring = String(json[..<nextIndex])
// 解析这部分残缺的 JSON
let generatedContent = try GeneratedContent(json: substring)
let content = try PartiallyGenerated(generatedContent)
// 发送给流
continuation.yield(content)
index = nextIndex
// 假装网络延迟(或者 AI 正在思考人生)
try await Task.sleep(for: delay)
}
continuation.finish()
}
}
}
}

现在,我们可以在 Playground 里先试玩一下,就像在安全屋里整理背包:
swift
import Playgrounds
#Playground {
let json = #"{"name": "Trisha", "profile": "A playful and curious cat", "age": 8}"#
for try await catProfile in CatProfile.streamResponse(from: json) {
// 你会看到控制台里数据一点点打印出来
print(catProfile)
}
}

最后,为了在 SwiftUI 预览里看到这一幕,我们需要一个包装视图(Wrapper View)来承载这个流。

因为 Xcode 预览不支持直接预览异步流,我们需要一个容器来接收数据。
swift
private struct WrapperView: View {
@State private var catProfile: CatProfile.PartiallyGenerated?
var body: some View {
ZStack {
if let catProfile {
CatProfileView(catProfile: catProfile)
}
}
.task {
// 开始模拟!
do {
let json = #"{"name": "Trisha", "profile": "A playful and curious cat", "age": 8}"#
// 就像看电影一样,数据流开始播放
for try await catProfile in CatProfile.streamResponse(from: json) {
self.catProfile = catProfile
}
} catch {
print("Error generating cat profile: \(error)")
}
}
}
}
#Preview("Stream") {
WrapperView()
}
效果拔群!看着预览界面里的猫咪档案一行行浮现,就像看着希望在绝望中升起。


🚁 结局:逃出生天
"Leon!暴君把门撞开了!我们得走了!" Claire 的声音近在咫尺。

我猛地合上 MacBook Pro。代码跑通了,预览完美,UI 没有任何跳变。Foundation Models 改变了我们构建 SwiftUI 视图的方式,虽然处理 PartiallyGenerated 需要写一些类似于"胶带粘水管"的样板代码,但它让我们能精准控制 AI 生成内容的每一个中间状态。

在 AI 时代,等待响应的过程不再是空白,而是用户体验的一部分。
我抓起桌上的霰弹枪,回头看了一眼屏幕上最后定格的猫咪档案预览。那是我们在地狱中唯一的一抹温柔。
"走吧,Claire。" 我拉动枪栓,嘴角上扬,"为了猫咪。"
(枪声响起,屏幕渐黑)
🔴 YOU SURVIVED
(Result: S Rank)
