SwiftUI 核心要义
SwiftUI essentials
2024年6月10日
一句话判断
这场 Session 用一个完整的宠物 app 示例系统性地梳理了 SwiftUI 的三个核心特质——声明式、组合式、状态驱动——如果你对 SwiftUI 的理解还停留在”写 UI 的新语法”,这场演讲能帮你建立正确的思维模型。
这场 Session 讲了什么
这是一场面向 SwiftUI 入门到中级开发者的基础梳理 Session。讲者用一个”宠物 trick 排行榜” app 作为贯穿始终的示例,从最基础的 View 概念讲到 List、NavigationView、自定义组件、状态管理,最后到和其他框架的集成。
核心内容分三个层面。第一层是 View 的本质:SwiftUI 的 View 是声明式的描述,不是长期存在的对象实例。每次状态变化时,SwiftUI 会重新求值 View body,生成新的描述,然后高效地更新渲染。第二层是组合:通过容器视图(HStack、VStack、List)和 View Modifier 把简单视图组合成复杂界面。第三层是状态驱动:用 @State、@Bindable、@Environment 等属性包装器声明数据依赖,SwiftUI 自动处理 UI 更新。
Session 还花了不少篇幅讲 SwiftUI 如何与其他框架共存——UIKit 互操作、SwiftData 持久化、Widget 扩展——强调 SwiftUI 天生支持增量采用,不需要整个 app 重写。
值得深挖的点
View 是值类型——这不只是实现细节
SwiftUI 的 View 被定义为 struct 而非 class,这个选择背后的含义比表面看起来深远得多。因为 View 是值类型,每次 body 被求值时产生的是一份轻量的”描述快照”,而不是一个需要被持有和管理的对象。SwiftUI 在内部维护了一个高效的持久化数据结构来 diff 这些快照,计算出最小的 UI 变更集,然后批量应用。
这意味着拆分 View 不会带来性能开销。你可以放心地把一个 200 行的 body 拆成 5 个子 View,不需要担心额外的对象创建成本——因为 View 本身就不是传统意义上的”对象”。这个特性直接鼓励了良好的代码组织:每个 View 只关注自己的职责,状态通过参数传入,回调通过 closure 传出。
讲者用了一个很直观的比喻:命令式编程像训狗——“过来、拿球、跑向本垒”每一步都要你发出指令。声明式编程像描述你想要的结果——“看到 Rufus 打出全垒打”,然后让框架去完成中间步骤。两种范式不是对立的,SwiftUI 中 Button 的 action 就是命令式代码,但你只需要在需要的地方写命令式代码,而不是到处都是。
List 和 ForEach 的组合模式
List 在 SwiftUI 中远不只是 UITableView 的替代品。Session 展示了一个容易被忽略的用法:List 的集合初始化器本质上是 ForEach 的语法糖,这意味着你可以用它来组合多个数据源。
比如你的 app 有”我的宠物”和”别人的宠物”两个列表,用 Section + ForEach 的组合就能轻松实现分组展示,不需要维护一个合并后的数组。再加上 .swipeActions 修饰符,每个 row 可以有滑动操作。这种组合模式在 UIKit 中需要大量的 datasource 配置代码,在 SwiftUI 中就是几行声明。
代码片段
自定义 View + View Modifier 的组合
把重复的 UI 模式封装成可复用组件。
// 自定义行视图:宠物信息卡片
struct PetRowView: View {
let pet: Pet
// 将复杂的图片处理逻辑提取为私有计算属性
private var profileImage: some View {
Image(pet.photoName)
.resizable()
.scaledToFill()
.frame(width: 60, height: 60)
.clipShape(Circle())
.shadow(radius: 3)
.overlay(Circle().stroke(Color.green, lineWidth: 2))
}
var body: some View {
HStack {
profileImage
VStack(alignment: .leading) {
Text(pet.name)
.font(.headline)
Text("\(pet.tricks.count) tricks learned")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
}
}
坑:View Modifier 的顺序很重要。.clipShape 必须在 .shadow 之前,否则阴影会被裁掉。.overlay 放最后,才能画在裁剪后的圆形上方。
多数据源的分组 List
List {
Section("My Pets") {
ForEach(myPets) { pet in
PetRowView(pet: pet)
.swipeActions(edge: .trailing) {
Button("Delete", role: .destructive) {
deletePet(pet)
}
}
}
}
Section("Everyone Else's Pets") {
ForEach(otherPets) { pet in
PetRowView(pet: pet)
}
}
}
状态驱动的基础模式
struct PetListView: View {
// @State 管理视图的本地状态
@State private var pets: [Pet] = []
@State private var showingAddPet = false
var body: some View {
NavigationStack {
List(pets) { pet in
PetRowView(pet: pet)
}
.navigationTitle("Pet Tricks")
.toolbar {
Button("Add Pet") {
showingAddPet = true
}
}
.sheet(isPresented: $showingAddPet) {
AddPetView { newPet in
pets.append(newPet)
}
}
}
}
}
坑:@State 只适合管理当前 view 的私有状态。如果多个 view 需要共享数据,应该用 @Observable + @Environment 或 @Bindable。把共享数据放在 @State 里会导致状态在 view 重建时丢失。
最佳实践
从 UIKit 迁移: 不要试图一次性重写。SwiftUI 的 UIHostingController 让你可以在 UIKit 中嵌入单个 SwiftUI 视图,反过来 UIViewRepresentable 也能在 SwiftUI 中使用已有的 UIKit 组件。找到一个独立的、边界清晰的 feature(比如设置页面、新手引导),先用 SwiftUI 实现它,验证团队的学习曲线和开发效率。
新项目: 直接用 SwiftUI。Apple 内部已经在大量新 app 中使用 SwiftUI,框架的成熟度已经不是问题。搭配 SwiftData 做数据持久化,用 @Query 自动同步 UI 和数据,整个技术栈都是声明式的。但要注意一些高级自定义场景可能还是需要回退到 UIKit,比如高度自定义的滚动行为、复杂的触摸事件处理。
代码组织: 善用 View 的值类型特性。把大 View 拆成小 View,每个 View 接受明确的输入参数。不要在一个 View 的 body 里写 200 行代码——拆成 5 个子 View 不仅更易维护,对性能也没有负面影响。
还有什么值得关注
- SwiftUI 的
#Preview宏可以在 Xcode 中同时预览所有 View 变体(Light/Dark、不同尺寸),开发效率提升明显 @Observable宏(iOS 17+)比传统的ObservableObject+@Published写法更简洁,不需要手动标记哪些属性需要触发更新- SwiftUI 与 SwiftData 的深度集成(
@Query、@Model)让数据驱动的 app 开发变得非常轻量,Core Data 的 boilerplate 基本可以抛弃