本节摘要:SwiftUI 的世界只有两种零件:遵循 View 协议的视图(描述树上的节点)与修饰符(把视图包一层的改性胶囊)。本节拆 View 协议的三个关键设定——body 只读、some View 不透明类型、视图是值;再画"洋葱图"讲清修饰符链的包裹结构与顺序敏感;最后用自定义修饰符把阳台花园的卡片样式收编成可复用基因。
5.1 说 body 是"状态到图纸的函数",现在把它当协议正式看一遍——View 协议(2.6 的嫁接术在框架级的应用)只要求一件事:
struct PlantCardView: View { let plant: Plant var body: some View { // 唯一要求:一个返回"某种视图"的只读属性 Text(plant.name) } }
三个设定值得逐个咀嚼。其一,body 是只读计算属性:你不能在 body 里写 plant.water()——描述树里没有"执行命令"的位置,改状态是按钮动作闭包的事。这条限制逼出好架构:界面长什么样与状态怎么变,被语法层面分了家。其二,返回类型是 some View:读作"某种视图"——调用方不需要知道具体是 Text 还是 VStack(2.8 泛型的应用),编译器知道就行;这让苹果能在不改你代码的前提下换底层实现。其三,视图是结构体:值类型、随建随弃——框架每帧可能重建视图值,所以视图里不该有昂贵初始化,重活放状态对象(5.3)。
组合是第三个基因。视图里可以装视图,函数可以拆视图:
struct PlantDetailView: View { let plant: Plant var body: some View { VStack(alignment: .leading, spacing: 12) { titleBar // 小视图也是计算属性,body 保持可读 statusLine waterButton } .padding() } private var titleBar: some View { HStack { Text(plant.name).font(.title.bold()) Spacer() Text(plant.kind.rawValue).foregroundColor(.secondary) } } private var statusLine: some View { Text(plant.statusText) // 模型自带文案(3.1 的纪律原样保留) } private var waterButton: some View { Button("浇水") { print("已给 \(plant.name) 浇水") } .buttonStyle(.borderedProminent) } }
修饰符(modifier)不"修改"视图——它把原视图包进一层新视图,像给洋葱加一层皮:
Text("还剩 2 天") .padding() // 皮一:留白 .background(.green) // 皮二:背景(铺在"皮一"的后面) .cornerRadius(8) // 皮三:圆角(切的是"皮二")

顺序敏感由此而来——同一个修饰符集合,顺序不同结果不同:
// 顺序一:绿背景包住"文字加留白"——绿块大一圈 Text("还剩 2 天").padding().background(.green) // 顺序二:留白包住"文字加绿背景"——绿块紧贴文字,四周露出页面底色 Text("还剩 2 天").background(.green).padding() // 实测差异(用边框显形): Text("A").padding().background(.green).border(.red) // 红框在绿块外沿 Text("B").background(.green).padding().border(.red) // 红框与绿块之间隔了一圈透明留白
frame 是顺序敏感的重灾区:frame(width: 200) 写在 background 前后,得到的是"绿底 200 宽"还是"绿底裹文字外加 200 宽容器",肉眼极易混。排错口诀已经写在图里:从上往下读链,从里往外读洋葱。
同一种卡片样式散落各页是维护灾难,ViewModifier 把它收编成基因:
struct GardenCard: ViewModifier { // 修饰符本质:旧的 Gone,包一层新的 func body(content: Content) -> some View { // content 是被包裹的原视图 content .padding(14) .background(.white) .cornerRadius(14) .shadow(color: .black.opacity(0.08), radius: 6, y: 2) } } extension View { // 挂到 View 协议扩展上,调用像原生 func gardenCard() -> some View { modifier(GardenCard()) } } // 用起来与系统修饰符无差别 struct FavoriteGridView: View { let plants: [Plant] var body: some View { VStack(spacing: 12) { ForEach(plants) { p in PlantRow(plant: p).gardenCard() // 一行即成卡 } } } } // 渲染结果:每行白底圆角投影卡片,样式改一处全局生效
案例收尾走一遍完整过程。背景:详情页与收藏页各写了一份卡片样式,设计改圆角(14 改 16)要改两处。操作:抽成 GardenCard 修饰符,两处调用。结果:改 16 只动一行,两页同步。解读:修饰符复用的是"包裹逻辑",与 UIKit 时代抽配置函数(3.1 的 makeWaterButton)同构,但声明式让它成了语言级能力——不用传工厂函数,直接点出来。变式:带参数的修饰符(圆角、配色做成属性)进一步模板化;当"修饰符"开始带状态,就该升级成自定义视图了——那是下一节数据流的开场。
⚠️ 常见坑:在 body 里调用带副作用的函数(写文件、发请求)。body 会被框架任意多次求值,副作用就任意多次执行——网络请求进 body 是 SwiftUI 新手榜的翻车第一名,正确位置是任务修饰符与状态对象(5.3)。
图纸会画了,水还没通——下一节把状态与数据流接进视图树。