浣熊市生存手册:在 Xcode 预览中驯服“支离破碎”的 AI 流式数据

这是一个关于在那座被诅咒的城市------浣熊市(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)

相关推荐
曾经我也有梦想2 天前
ObservableObject @Published @ObservedObject那些事
swiftui
曾经我也有梦想2 天前
@Binding 的那些事
swiftui
曾经我也有梦想2 天前
@state的一些琐事
swiftui
东坡肘子4 天前
我的 App 审核被卡了? -- 肘子的 Swift 周报 #128
人工智能·swiftui·swift
1024小神8 天前
记录xcode项目swiftui配置APP加载启动图
前端·ios·swiftui·swift
WaywardOne8 天前
SwiftUI中修饰符的顺序直接影响视图最终效果
ios·swiftui·ui kit
苏渡苇8 天前
Stream.collect() 的花式玩法:Collector.of() 自定义收集器
java·stream·jdk21·collector·jdk8+·自定义收集器
东坡肘子11 天前
50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127
人工智能·swiftui·swift
C雨后彩虹13 天前
深入探索Java Stream:6个复杂业务场景下的高效实现方案
java·多线程·stream·同步·异步