1. 从“SwiftUI-Agent-Skill”这个名字说起:它到底想解决什么问题
第一次看到“SwiftUI-Agent-Skill”这个组合词,很多人会愣一下:SwiftUI 是苹果生态里做界面的框架,Agent 是智能体,Skill 是技能,这三个词拼在一起,到底指什么?我个人的理解是,它描述的是一类很具体的东西——让一个智能体具备“用 SwiftUI 写界面”这项技能。换句话说,它不是某个现成的库,也不是某个官方框架,而是一个能力封装的方向:把 SwiftUI 的开发知识、代码生成规则、常见布局模式、状态管理套路,打包成智能体可以调用、可以复用、可以稳定输出的一套“技能包”。
为什么这件事值得单独拿出来聊?因为 SwiftUI 这门技术有个很尴尬的特点:入门极快,精通极难,而中间那段“能写但写不好”的区间特别长。你让一个智能体去生成 UIKit 代码,它往往能给你一堆能跑的样板;但你让它生成 SwiftUI 代码,它很容易写出“语法正确、逻辑别扭、状态乱飞、预览报错”的东西。原因在于 SwiftUI 是声明式的,它的坑不在语法层面,而在数据流、视图身份、生命周期、修饰符顺序这些隐性规则上。一个没有经过专门约束的智能体,几乎必然会在这些地方翻车。
所以“SwiftUI-Agent-Skill”这个标题背后,真正的核心需求是:如何把 SwiftUI 的开发经验,转化成智能体可执行的、可验证的、可迭代的技能规范。它适合几类人看:一是正在做 AI 辅助编程工具、想让智能体稳定产出 SwiftUI 代码的开发者;二是自己用智能体写 iOS 界面、但被各种奇怪 bug 折磨的独立开发者;三是对“技能封装”这个方法论感兴趣、想迁移到其他框架(比如 Compose、Flutter)的技术人。哪怕你只是单纯想让自己手写 SwiftUI 的水平上一个台阶,这套思路也能帮你把脑子里的经验结构化。
我接下来会从技能拆解、状态管理约束、布局与修饰符规则、预览与调试、以及技能迭代这几个角度,把这件事讲透。全程按我实际折腾下来的经验说,不绕弯子。
2. 把 SwiftUI 拆成智能体能“吃下去”的技能单元
2.1 为什么不能直接把整个 SwiftUI 文档丢给智能体
很多人第一反应是:技能包嘛,把 SwiftUI 的官方文档、常用代码片段、Stack Overflow 高赞答案全塞进知识库不就行了?我试过,效果很差。原因有三个。第一,文档是给人看的,不是给智能体执行决策用的。文档里大量“你可以……也可以……”的并列选项,智能体面对这种模糊性时,会随机挑一个,导致同一个需求每次生成的风格都不一样。第二,SwiftUI 的很多规则是“反直觉”的,比如@State应该放在哪个层级、@Binding什么时候必须用、ObservableObject和@Observable怎么选,这些在文档里是分散的,智能体很难自己串起来。第三,缺少“错误模式”的标注。人写代码踩坑后会记住“这样写会崩”,但文档不会告诉你“这样写预览会挂”,智能体自然也不知道要避开。
所以正确的做法是:把 SwiftUI 拆成一个个边界清晰、输入输出明确、带正反例的技能单元。每个单元不是一段知识,而是一个“决策规则 + 代码模板 + 禁忌清单”的三元组。
2.2 我实际拆出来的六类技能单元
经过几轮迭代,我把 SwiftUI 相关能力拆成了下面这六类。这个拆法不是拍脑袋,而是按“智能体在生成代码时最容易出错的环节”倒推出来的。
| 技能单元 | 核心职责 | 典型触发场景 | 最容易翻车的点 |
|---|---|---|---|
| 视图结构技能 | 决定用 VStack/HStack/ZStack/List/Grid 中的哪个 | 用户描述一个界面布局 | 嵌套过深、滥用 ZStack |
| 状态管理技能 | 选择 @State/@Binding/@Observable/@Environment | 涉及数据变化、跨视图传值 | 状态提升层级错误 |
| 修饰符技能 | 决定修饰符的书写顺序和组合 | 样式、间距、圆角、阴影 | 顺序错导致样式失效 |
| 导航技能 | NavigationStack/NavigationSplitView/Sheet | 页面跳转、弹窗 | 路径管理混乱 |
| 数据流技能 | 异步加载、Combine、async/await | 网络请求、本地存储 | 主线程更新、任务取消 |
| 预览技能 | #Preview 宏、预览数据构造 | 每个视图都要能预览 | 预览崩溃、假数据缺失 |
这六类里,状态管理技能和修饰符技能是重灾区。我见过太多智能体生成的代码,视图结构没问题,但状态一改就整个界面重绘、动画乱跳、输入框失焦。这些问题的根因几乎都落在状态层级和修饰符顺序上。
2.3 每个技能单元应该长什么样
拿“状态管理技能”举例,一个合格的技能单元不能只写“用 @State 管理本地状态”。它应该包含下面这些内容,我按实际写的格式给你看:
技能名称:本地简单状态选择 触发条件:视图内部有一个值会变化,且不需要跨视图共享 决策规则: 1. 如果值类型是值类型(String/Int/Bool/struct),用 @State 2. 如果值需要在子视图修改,父视图用 @State,子视图用 @Binding 3. 如果值需要在多个不相关视图共享,提升到 @Observable 类 4. 如果值是数组且频繁增删,考虑用 @State 包裹数组,但注意 identity 反例: - 不要在 View 的 body 里直接修改 @State,必须通过 action - 不要把 @State 放在 computed property 里 - 不要用 @State 存引用类型的大对象 验证方式:改值后预览是否实时更新、是否触发预期动画你看,这种格式智能体是能“执行”的,因为它有明确的 if-then 分支和反例。而“用 @State 管理状态”这种话,智能体读完还是不知道边界在哪。
提示:技能单元里的“反例”比“正例”更重要。正例智能体本来就能编出来,反例才是你真正要灌输的经验。
2.4 技能单元之间的依赖关系要显式声明
还有一个容易被忽略的点:技能单元不是孤立的。状态管理技能依赖视图结构技能的输出,修饰符技能又依赖状态管理技能的结果。如果不声明依赖,智能体可能先决定用 @Binding,结果发现父视图根本没定义 @State,整个链路就断了。
我的做法是在每个技能单元头部加一行依赖:xxx技能,并在生成流程里做拓扑排序。这样智能体在生成代码前,会先确认前置技能是否已经产出结果。这个机制听起来简单,但实测下来,它把“生成代码后才发现状态对不上”的概率降低了一大半。
3. 状态管理:智能体最容易翻车的地方,也是技能包价值最高的地方
3.1 为什么状态管理是 SwiftUI 智能体的“阿喀琉斯之踵”
如果只能给 SwiftUI 智能体技能包保留一个模块,我会毫不犹豫选状态管理。原因很直接:SwiftUI 的视图是值类型,每次状态变化都会重新创建视图结构,而智能体往往用命令式的思维去理解它。命令式思维下,“我改了这个变量,界面应该局部更新”;但 SwiftUI 的实际行为是“状态变了,body 重新执行,diff 后更新”。这两者的差异,导致智能体生成的代码经常出现下面这些症状:
- 输入框每输入一个字符就失去焦点
- 列表滚动位置莫名重置
- 动画作用在错误的视图上
- 子视图修改了值,父视图没反应
- 预览里正常,真机上一操作就崩
这些症状背后,几乎都是状态归属和身份(identity)的问题。而身份问题,恰恰是文档里讲得最含糊、智能体最难自己悟出来的部分。
3.2 我总结的状态选择决策树
为了让智能体稳定做决策,我把状态选择做成了一棵决策树。这棵树不是理论推导,是我在实际项目里反复修正出来的。核心逻辑是三个问题:这个值归谁管?谁需要读它?谁需要写它?
问题1:这个值只在当前视图内部使用吗? 是 → 用 @State 否 → 进入问题2 问题2:这个值需要被子视图修改吗? 是 → 父视图 @State + 子视图 @Binding 否 → 进入问题3 问题3:这个值需要在多个不相关视图间共享吗? 是 → 用 @Observable 类 + @Environment 或直接注入 否 → 进入问题4 问题4:这个值是全局配置或环境相关吗? 是 → 用 @Environment 否 → 重新审视问题1,可能判断有误这棵树的关键在于问题2和问题3的区分。很多智能体一看到“跨视图”就直接上 @Observable,结果一个简单的父子传值被搞成了全局状态,代码复杂度飙升。实际上,父子之间的单向或双向传值,@State + @Binding 就够了,根本不需要引入观察者对象。
3.3 @Observable 和 ObservableObject 的选择,别让智能体自己猜
Swift 5.9 之后引入了 @Observable 宏,和老的 ObservableObject + @Published 形成了两套体系。智能体在这两者之间经常摇摆,有时候同一个文件里混用,导致编译警告甚至运行时行为不一致。我的处理方式是:在技能包里硬性规定优先级。
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新项目,最低部署版本 iOS 17+ | @Observable | 代码更简洁,性能更好,无需 @Published |
| 需要兼容 iOS 16 及以下 | ObservableObject + @Published | @Observable 不可用 |
| 已有代码库大量使用 ObservableObject | 保持 ObservableObject | 混用成本高于收益 |
| 需要精细控制哪些属性触发更新 | @Observable + 手动 willSet/didSet | 宏默认全部追踪,特殊场景需干预 |
这个表直接写进技能单元,智能体每次做选择时先查表,不再自由发挥。实测下来,混用问题基本消失了。
3.4 一个真实的反例:输入框失焦是怎么发生的
我拿一个具体场景说明状态管理技能的价值。假设需求是“一个搜索框,输入时实时过滤列表”。智能体很容易生成这样的代码:
struct SearchView: View { @State private var query = "" @State private var results: [Item] = [] var body: some View { VStack { TextField("搜索", text: $query) .onChange(of: query) { _, newValue in results = filterItems(newValue) } List(results) { item in Text(item.name) } } } }这段代码看起来没问题,但实际跑起来,如果filterItems是耗时操作,或者results的 identity 不稳定,输入框就会卡顿甚至失焦。更隐蔽的问题是:onChange里直接改results,会触发 body 重新执行,而 body 重新执行时TextField被重建,焦点就丢了。
正确的做法是把过滤逻辑和视图更新解耦,比如用@Observable的 ViewModel 持有 query 和 results,或者用.searchable配合task(id:)。这个反例我直接写进了技能单元,智能体再遇到“搜索框 + 实时过滤”的需求时,会优先选择解耦方案,而不是在 onChange 里直接改状态。
注意:状态管理技能里一定要包含“哪些操作会触发 body 重执行”的说明。智能体不知道这个,就会在 body 里做各种副作用操作。
3.5 状态提升的边界:什么时候该把状态往上提
状态提升是 SwiftUI 里另一个高频决策点。智能体常见的错误是“过度提升”——把所有状态都提到根视图,导致根视图臃肿、子视图全是 Binding。我的经验规则是:状态应该放在“所有需要读它的视图的最近公共祖先”上,而不是更上面。
举个例子,一个详情页里有个收藏按钮,收藏状态只被按钮自己和详情页头部读取。那这个状态就应该放在详情页视图,而不是 App 级别的环境对象里。只有当收藏状态需要被列表页、详情页、个人中心同时读取时,才提升到全局。
这个规则我写成了一句话放进技能包:“状态的最小公共祖先原则”。智能体每次决定状态位置时,先找出所有读取该状态的视图,然后取它们的最近公共祖先。这个规则简单,但效果立竿见影。
4. 布局与修饰符:顺序错了,一切都错了
4.1 修饰符顺序为什么是 SwiftUI 的“隐藏语法”
SwiftUI 的修饰符(modifier)看起来是链式调用,但它和普通的方法链有本质区别:每个修饰符都会返回一个新的视图类型,顺序不同,类型不同,行为也不同。这一点智能体极难自己理解,因为它看到的只是.padding().background()这样的文本,不知道背后类型在变。
最经典的例子是.padding()和.background()的顺序:
// 方案A:背景只包住内容 Text("Hello") .background(Color.blue) .padding() // 方案B:背景包住内容和内边距 Text("Hello") .padding() .background(Color.blue)方案A里,蓝色背景紧贴文字;方案B里,蓝色背景包含了内边距,看起来像一块带内边距的色块。这两个方案没有对错,但智能体如果不知道区别,就会随机选一个,导致界面和预期不符。
4.2 我整理的修饰符顺序规则
为了让智能体稳定输出,我把修饰符按“作用层次”分成了四组,并规定了大致的书写顺序。这个顺序不是绝对的,但覆盖了 90% 的场景。
| 顺序 | 修饰符类别 | 常见修饰符 | 作用 |
|---|---|---|---|
| 1 | 布局修饰符 | frame, padding, offset, position | 决定视图在父容器中的位置和大小 |
| 2 | 外观修饰符 | background, foregroundStyle, clipShape, overlay | 决定视图长什么样 |
| 3 | 效果修饰符 | shadow, blur, opacity, scaleEffect | 决定视图的视觉效果 |
| 4 | 交互修饰符 | onTapGesture, gesture, disabled | 决定视图如何响应交互 |
核心原则是:先定位置和大小,再定外观,再加效果,最后加交互。因为位置和大小会影响外观的绘制区域,外观又会影响效果的作用范围。如果顺序反了,比如先加 shadow 再改 frame,阴影可能被裁掉或者位置错乱。
4.3 布局容器的选择:VStack 不是万能的
智能体有个很强的倾向:不管什么布局都用 VStack 套 HStack。这在简单界面里没问题,但一旦遇到列表、网格、自适应布局,就会写出嵌套地狱。我在技能包里明确规定了容器选择的优先级:
- 需要滚动的一维列表 →
List或ScrollView + LazyVStack - 需要二维网格 →
LazyVGrid/LazyHGrid - 需要自适应排列 →
ViewThatFits或Layout协议 - 需要层叠 →
ZStack,但层级不超过 3 层 - 简单线性排列 →
VStack/HStack
其中List和LazyVStack的区别特别重要。List自带复用、分隔线、滑动操作,适合数据量大的场景;LazyVStack更灵活但需要自己处理复用和分隔。智能体经常在该用 List 的地方用 LazyVStack,导致性能问题。
4.4 一个布局翻车案例:嵌套过深导致的性能问题
我遇到过智能体生成的一个界面,一个简单的设置页,嵌套了 7 层 VStack/HStack。代码能跑,但滚动时明显掉帧。排查后发现,每一层嵌套都会增加布局计算的开销,7 层叠加后,每个 cell 的布局时间超标。
修复方式是把中间几层用Group扁平化,或者用LabeledContent这类语义化容器替代手写的 HStack。这个案例让我意识到,技能包里必须包含“嵌套深度警告”:任何视图嵌套超过 5 层,智能体应该主动提示重构。
// 嵌套过深的写法 VStack { HStack { VStack { HStack { VStack { Text("设置项") } } } } } // 扁平化后 LabeledContent("设置项", value: "值")LabeledContent是 iOS 16 引入的,专门用于“标签 + 值”的布局,一行顶上面一堆嵌套。智能体知道这个组件后,生成的代码质量明显提升。
4.5 修饰符技能里的“禁忌清单”
除了顺序规则,我还整理了一份禁忌清单,直接写进技能包。这些是智能体最容易犯、但文档里不会强调的错误:
- 不要在
List的行里用.frame(maxHeight: .infinity),会导致行高异常 - 不要在
ScrollView里嵌套另一个ScrollView同方向滚动 - 不要对
TextField用.disabled(true)后又期望它能滚动 - 不要在
ZStack里放太多全屏视图,会触发离屏渲染 - 不要用
.offset做布局,offset 只影响绘制不影响布局,应该用 padding 或 frame
每一条禁忌都配一个反例和修复方案。智能体在生成代码后会做一次自检,命中禁忌就重新生成。这个自检机制把很多低级错误挡在了输出之前。
5. 预览与调试:让智能体生成的代码“可验证”
5.1 为什么预览是 SwiftUI 智能体的“测试用例”
SwiftUI 有个别的框架没有的优势:预览(Preview)可以在不运行 App 的情况下验证视图。这对智能体来说太重要了,因为智能体没法真正跑 App,但它可以生成预览代码,然后通过某种方式验证预览是否能编译、是否能渲染。
我的做法是:要求智能体为每个生成的视图都附带一个 #Preview 块,并且预览里必须提供完整的假数据。这看起来是额外工作,但它逼着智能体把视图的依赖关系想清楚。一个视图如果没法构造出预览数据,说明它的依赖太复杂,需要重构。
#Preview { SearchView( viewModel: SearchViewModel( items: [ Item(id: UUID(), name: "示例一"), Item(id: UUID(), name: "示例二") ] ) ) }这个预览块本身就是一种验证:如果智能体生成的视图需要一堆环境对象、绑定、异步数据才能渲染,那预览就写不出来,智能体就会被迫简化依赖。
5.2 预览崩溃的常见原因和排查顺序
智能体生成的预览经常崩溃,我总结了一个排查顺序,也写进了技能包:
- 检查是否有 @Environment 未注入:预览里没有环境值,视图一读就崩
- 检查是否有 @Binding 未提供:预览需要
.constant()或真实的 @State - 检查是否有异步数据在 body 里同步读取:预览不会等异步完成
- 检查是否有 fatalError 或强制解包:预览环境更容易触发
- 检查是否有 @Observable 对象未初始化:宏生成的代码在预览里可能行为不同
这个顺序是按“出现频率”排的。实测下来,前两条能解决 70% 的预览崩溃。
5.3 用预览驱动技能迭代
预览还有一个隐藏价值:它是技能包迭代的反馈信号。每次智能体生成代码后,如果预览编译失败或渲染异常,这个失败案例就可以反哺技能包。比如某个修饰符组合反复导致预览崩溃,那就把它加入禁忌清单;某个状态写法反复导致预览不更新,那就修正状态选择决策树。
我实际的做法是维护一个“失败案例库”,每个案例记录:触发场景、生成的代码、失败现象、根因、修正后的规则。这个库就是技能包不断进化的燃料。没有这个反馈闭环,技能包很快就会过时。
5.4 调试技能:让智能体学会“自我解释”
除了预览,我还要求智能体在生成代码后附上一段“决策说明”,解释它为什么这样写。比如:
决策说明: - 选择 @State 而非 @Observable,因为该状态只在当前视图使用 - 选择 List 而非 LazyVStack,因为数据量可能超过 100 条 - 修饰符顺序:先 padding 后 background,因为需要背景包含内边距 - 预览提供了 3 条假数据,覆盖空列表和正常列表两种情况这段说明对使用者极有价值,因为它把智能体的“黑盒决策”变成了可审查的文本。如果说明里的理由不对,使用者可以立刻发现并修正技能包。这比单纯看代码高效得多。
6. 技能包的迭代与迁移:从 SwiftUI 到其他框架
6.1 技能包不是写完就完,要建立反馈闭环
“SwiftUI-Agent-Skill”这类技能包最大的误区是把它当成一次性文档。实际上,技能包是一个活的系统,它的质量取决于反馈闭环的速度。我自己的闭环是这样的:
- 智能体生成代码
- 预览编译 + 渲染验证
- 人工抽查关键决策
- 失败案例归因
- 修正技能单元
- 回归测试历史案例
这个闭环里,第 4 步最关键。归因不能停在“这里错了”,要追到“哪条规则缺失或错误”。比如智能体用了 @ObservedObject 而不是 @State,不能只说“用错了”,要问“决策树里哪个分支没覆盖这个场景”。只有追到规则层面,修正才有意义。
6.2 技能单元的可迁移性:换框架要换什么
这套方法论不只适用于 SwiftUI。如果你要迁移到 Jetpack Compose 或 Flutter,技能单元的骨架可以保留,但内容要换。具体来说:
| 技能单元 | SwiftUI | Compose | Flutter |
|---|---|---|---|
| 状态管理 | @State/@Binding/@Observable | remember/mutableStateOf/ViewModel | StatefulWidget/Provider |
| 布局容器 | VStack/HStack/ZStack | Column/Row/Box | Column/Row/Stack |
| 修饰符 | modifier 链 | Modifier 链 | Widget 嵌套 |
| 预览 | #Preview | @Preview | hot reload |
| 导航 | NavigationStack | NavHost | Navigator |
迁移时,决策树的逻辑可以复用,但具体类型和 API 要替换。比如“状态最小公共祖先原则”在 Compose 里同样成立,只是实现从 @State 变成 remember。这种逻辑层的复用,才是技能包真正的价值。
6.3 我踩过的坑:技能包过度膨胀
最后说一个我自己的教训。刚开始做技能包时,我恨不得把所有 SwiftUI 知识都塞进去,结果技能包膨胀到几万字,智能体反而不知道该看哪条。后来我做了减法,只保留“智能体容易错且文档没讲清”的规则,把通用知识交给模型自己的预训练。技能包从几万字压缩到几千字,效果反而更好。
这个经验告诉我:技能包不是知识库,而是决策修正器。它的职责是纠正智能体的默认倾向,而不是替代智能体的基础知识。想清楚这一点,技能包的设计思路就清晰了。
提示:判断一条规则该不该进技能包,问自己两个问题:智能体默认会做错吗?文档里讲清楚了吗?两个都是“是”,才值得写进去。
6.4 后续可以扩展的方向
如果这套技能包要继续演进,我会往三个方向走。第一是增加“重构技能”,让智能体不仅能生成新代码,还能识别坏味道并重构,比如把嵌套过深的 VStack 扁平化、把散落的 @State 收敛到 ViewModel。第二是增加“性能技能”,识别可能导致掉帧的写法,比如在 body 里做重计算、滥用 GeometryReader。第三是增加“无障碍技能”,自动补充 accessibilityLabel、动态字体支持、对比度检查。这三个方向都是 SwiftUI 实际开发中的高频痛点,也是智能体目前表现最弱的地方。
我个人在实际操作中的体会是,技能包的价值不在于它覆盖了多少知识,而在于它把多少“隐性经验”变成了“显性规则”。每把一条踩坑经验写成规则,智能体的输出就稳定一分。这个过程没有终点,但每进一步,都能实实在在感受到效率的提升。