SwiftUI 中的堆栈、网格和大纲视图
Stacks, Grids, and Outlines in SwiftUI
2020年6月25日
一句话判断
SwiftUI 在 iOS 14 中补上了数据驱动的列表布局这块短板——LazyVStack/LazyHStack、Grid 以及 OutlineGroup 让你在大多数场景下不再需要 UICollectionView。
这场 Session 讲了什么
SwiftUI 在 2020 年引入了大量的布局容器。最核心的是 Lazy 系列堆栈:LazyVStack 和 LazyHStack。与普通的 VStack/HStack 不同,Lazy 版本只渲染屏幕可见区域的视图,这对于长列表的性能至关重要。
然后是 LazyVGrid 和 LazyVGrid(水平网格),提供了类似 CollectionView 网格布局的能力。你可以通过 GridItem 定义列(或行)的尺寸策略:固定尺寸(.fixed(100))、弹性尺寸(.flexible())或自适应尺寸(.adaptive(minimum: 80))。
最后是 OutlineGroup 和 List 的树形支持。OutlineGroup 可以递归地渲染嵌套数据结构(如文件系统),配合 List 的 children 参数可以创建可折叠的大纲视图。DisclosureGroup 则提供了手动控制展开/折叠的能力。
值得深挖的点
Lazy 的真正含义。 LazyVStack 不会预渲染所有子视图——它只创建可见区域及附近(预加载缓冲区)的视图。当用户滚动时,新进入可见区域的视图才会被创建,离开可见区域的视图会被销毁。这与 UICollectionView 的 Cell 复用机制类似,但 SwiftUI 自动处理了复用逻辑。
Grid 的三种尺寸策略。 .fixed(100) 每列固定 100pt;.flexible() 按可用空间等分;.adaptive(minimum: 80) 则尽可能多地放入至少 80pt 宽的列,剩余空间均匀分配。第三种策略最适合响应式布局——不同屏幕宽度会自动产生不同列数。
代码片段
import SwiftUI
// LazyVStack——只渲染可见区域
struct MessageListView: View {
let messages: [Message]
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(messages) { message in
MessageRow(message: message)
.onAppear {
// 滚动到底部附近时加载更多数据
if message == messages.last {
loadMoreMessages()
}
}
}
}
.padding()
}
}
}
// LazyVGrid——自适应网格布局
struct PhotoGridView: View {
let photos: [Photo]
// 自适应列:每列至少 100pt,系统自动计算列数
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 200), spacing: 8)
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(photos) { photo in
PhotoCell(photo: photo)
.aspectRatio(1, contentMode: .fill)
.cornerRadius(8)
}
}
.padding()
}
}
}
// OutlineGroup——树形数据展示
struct FileItem: Identifiable {
let id: String
let name: String
let children: [FileItem]?
}
struct FileOutlineView: View {
let fileSystem: [FileItem]
var body: some View {
List(fileSystem, children: \.children) { item in
Label(item.name, systemImage: item.children == nil ? "doc" : "folder")
}
.listStyle(SidebarListStyle())
}
}
// DisclosureGroup——手动控制展开折叠
struct SettingsOutlineView: View {
@State private var isExpanded = true
var body: some View {
DisclosureGroup("高级设置", isExpanded: $isExpanded) {
Toggle("启用通知", isOn: .constant(true))
Toggle("自动更新", isOn: .constant(false))
DisclosureGroup("隐私设置") {
Text("位置服务")
Text("相机权限")
}
}
}
}
最佳实践
- 超过 20 个元素的列表必须用 Lazy 版本。 普通 VStack/HStack 会一次性创建所有子视图,大量数据会导致卡顿和内存问题。
- 优先使用
.adaptive网格策略。 它能自动适配不同屏幕尺寸,避免在 iPad 上出现尴尬的单列或过宽列。 onAppear是加载更多数据的好时机。 在 Lazy 堆栈中,视图的onAppear只在实际渲染时触发,可以用来实现无限滚动。- OutlineGroup 适合静态树结构,DisclosureGroup 适合交互式折叠。 前者数据驱动,后者状态驱动。
还有什么值得关注
- LazyVStack 支持
pinnedViews参数,可以实现悬浮的 Section Header - Grid 的 spacing 参数控制行列间距,可以分别设置
- List 的
children参数自动生成展开折叠按钮 - 所有这些容器都支持 SwiftUI 的动画系统