——写给三年前写 Auto Layout 约束写到手抽筋的我
【短笺一|从“摆控件”到“叙事”】
那晚我在 storyboard 里拖了第六条约束,按钮还是偏一像素。我把屏幕一锁,换成 SwiftUI 起手的三行:
VStack(spacing: 12) {
TitleView(text: title)
Toggle("提醒我", isOn: $isOn)
PrimaryButton("保存", action: save)
}
.padding(.horizontal, 16)
UIKit 的工作像在摆家具:先放沙发,再推茶几,手里拿着卷尺;SwiftUI 像写分镜:镜头一、镜头二、镜头三,布局来自层次。真正的转折不是“写法更少”,而是把“界面==状态的函数”这件事当真:
UI = f(state)
-
你把“按下保存键 → 置灰按钮 → 显示 HUD → 跳转下一页”的一堆 imperative 操作,换成了状态流:
enum ScreenState { idle, saving, saved }
视图只描述各状态的样子,过渡交给 SwiftUI 的 diff 和动画系统。
-
你第一次发现,删掉一半的“隐藏/显示”布尔开关,界面反而更稳。
给过去的你:别先问“这个控件在哪儿”,先问“这个状态有哪些形态”。形态写对了,控件自己会排好队。
【短笺二|生命周期不再是“回调清单”】
UIKit 的脑图里有 viewDidLoad / viewWillAppear / viewDidAppear / …。SwiftUI 里,这张清单被撕碎,换成了三个钩子:
- onAppear:第一次渲染或再次进入视图时触发(注意:List 里的 cell 也会反复 appear)。
- task:与视图绑定的异步任务;当视图消失时自动取消。
- onChange(of:):状态观察器,告诉你“变化发生了,你要不要跟一下动作”。
struct DetailScreen: View {
@State private var model: Model?
@State private var isSaving = false
var body: some View {
content
.task { model = try? await api.fetch() }
.onChange(of: isSaving) { saving in
if saving { Analytics.log("begin_save") }
}
}
}
- 你不再“记着”何时解绑通知、何时取消网络请求;视图的可见性=任务的生命。
- 你第一次把“防内存泄漏”的 checklist 扔进了垃圾桶一半。
给过去的你:把副作用丢进 task,把监听丢进 onChange,把一次性的初始化丢进 onAppear。剩下的交给系统回收。
【短笺三|布局:从 Auto Layout 的“力学”到 Stack 的“文法”】
Auto Layout 是“力平衡学”:约束求解器在解联立方程;SwiftUI 是“语法树”:VStack/HStack/ZStack 先定了句法,对齐和优先级再调语义。
三条我常用的“句法”记忆法:
- “先骨架,后装饰”:先堆 Stack,后上 .padding/.background/.overlay。
- “让谁长,就给谁权”:Spacer() 是生长点,LayoutPriority 像优先生长权。
- “几何别太早知道”:GeometryReader 是最后手段,别拿它当万能 rAF(容易把父子布局搞反)。
常见坑两个:
- GeometryReader 把孩子拉成无限大:外包一层 VStack { child } 或者加 fixedSize(),别让它吞掉所有空间。
- List 的 ForEach 乱跳:id 不稳定导致 diff 误判,务必提供稳定主键。
ForEach(items, id: \.id) { row in
RowView(row)
}
// 千万不要 id: \.self(除非 self 是稳定的值类型主键)
给过去的你:Auto Layout 是“求解”,SwiftUI 是“表达”。表达清楚,求解自然发生。
【短笺四|导航:从 push/pop 到“路径”】
过去我在 UINavigationController 上 push/pop;现在我给 NavigationStack 一个路径数组:
struct AppNav: View {
@State private var path: [Route] = []
var body: some View {
NavigationStack(path: $path) {
List(items) { item in
NavigationLink(value: Route.detail(item.id)) {
Text(item.title)
}
}
.navigationDestination(for: Route.self) { route in
switch route {
case .detail(let id): DetailScreen(id: id)
case .editor(let id): EditorScreen(id: id)
}
}
}
}
}
- Deep Link = 直接改 path;
- 返回上一级 = path.removeLast();
- 回到根 = path.removeAll()。
SwiftUI 的导航不是命令式的“去那里”,而是声明现在在哪儿。
踩坑备忘:
- 在 iOS 16 的早期版本里,NavigationStack 与 sheet/alert 叠加时,dismiss 顺序可能乱。我的做法是:把所有呈现状态集中到一个 PresentationState,用 enum 表示哪种浮层在场,避免多个布尔开关互相打架。
【短笺五|状态源:@State、@Binding、@ObservedObject、@EnvironmentObject——别混】
我给自己画过一张小地图:
- @State:视图内部的真源头(值类型);
- @Binding:把源头的一根“水管”借给子视图;
- @ObservedObject:外部对象的观察(一次性订阅);
- @EnvironmentObject:全局供给站(跨层级注入)。
错误的做法:到处塞 @EnvironmentObject,结果视图哪里都能改全局状态,调试像找幽灵。
正确的做法:单一数据流,能局部就局部,顶层才注入环境。
final class Session: ObservableObject {
@Published var user: User?
@Published var premium: Bool = false
}
@main
struct AppMain: App {
@StateObject private var session = Session()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(session) // 只注入一次
}
}
}
给过去的你:任何能 @State 解决的,不要上 ObservableObject;任何能传 @Binding 的,不要上 EnvironmentObject。
【短笺六|动画:不是“加个 options”,而是“状态过渡”】
UIKit 的动画像“事务”:UIView.animate(withDuration:…) { // 改属性 };SwiftUI 的动画像“声明”:这个状态的变化应该被动画化。
两种常见写法:
// 方式 A:在状态改变时包裹
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
isExpanded.toggle()
}
// 方式 B:让某个值的变化总是带动画
Text(value.description)
.animation(.easeInOut, value: value)
- 用 .matchedGeometryEffect 做跨视图过渡时,id 必须稳定;
- 复杂序列动画别硬顶在一处,用 phase 枚举把阶段拆开,状态驱动顺序更稳。
【短笺七|性能与“误会”】
SwiftUI 性能差?多半是身份误会和重建过度:
- 误会一:把大模型塞进 @State,每次变动都触发全量 diff。解法:拆粒度,或把大对象放进 ObservableObject 的小 @Published。
- 误会二:body 里做重活(日期格式化、图片解码)。解法:搬到 ViewModel 或用 @MainActor + 缓存。
- 误会三:ForEach 没稳定 id,List 频繁重用导致滚动丢焦点。
- 误会四:在 onAppear 写单例初始化,cell 往返时反复执行。加一个 once 标记或搬到更高层。
诊断工具箱:
- Instruments 里的 SwiftUI 模板 + Time Profiler;
- Xcode → Debug View Hierarchy 看 diff;
- Transactions 插件可视化动画事务;
- OSLog + Logger 给关键状态变化打点(不要 print 洪水)。
【短笺八|桥接:不是叛逃,是请老伙计来帮忙】
SwiftUI 不会让你当场造轮子。真遇到“UIKit 更合适”的地方,拉一座桥:
struct BlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIVisualEffectView {
UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) { }
}
或把一个复杂的 UIViewController 收进来:
struct ScannerSheet: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ScannerVC { ScannerVC() }
func updateUIViewController(_ vc: ScannerVC, context: Context) { }
}
要诀:桥接层只做桥接,不要把业务逻辑塞进 Representable;让 SwiftUI 仍然持有状态的源头。
【尾注|我给自己的三条铁律】
- 视图是结果,不是过程:别在视图里讲故事,用状态让故事自己发生。
- 一个事实只存一处:任何需要“同步两份”的状态,早晚出 bug。
- 可撤销优先:重构前先把测试和日志补齐;动画、导航、全局状态的改动都要能回退。
——
后来我回看那堆 Auto Layout 代码,心里并没有不耐烦。它们像旧城里的巷子,拐来拐去,却有味道。SwiftUI 像新修的环线,一圈绕下来,清楚利落。
真正的革命不是“把巷子全拆了”,而是学会什么时候走巷子,什么时候上环线。当你第一次把“界面==状态的函数”放在脑子最前面,SwiftUI 这条路就不再陌生。你会发现,手上写的,已经不是控件,而是场景。