Stacks, Grids, and Outlines in SwiftUI
Swift & UI 进阶 26m

SwiftUI 中的堆栈、网格和大纲视图

Stacks, Grids, and Outlines in SwiftUI

2020年6月25日

在 Apple 官方观看视频

一句话判断

SwiftUI 在 iOS 14 中补上了数据驱动的列表布局这块短板——LazyVStack/LazyHStack、Grid 以及 OutlineGroup 让你在大多数场景下不再需要 UICollectionView。

这场 Session 讲了什么

SwiftUI 在 2020 年引入了大量的布局容器。最核心的是 Lazy 系列堆栈:LazyVStackLazyHStack。与普通的 VStack/HStack 不同,Lazy 版本只渲染屏幕可见区域的视图,这对于长列表的性能至关重要。

然后是 LazyVGridLazyVGrid(水平网格),提供了类似 CollectionView 网格布局的能力。你可以通过 GridItem 定义列(或行)的尺寸策略:固定尺寸(.fixed(100))、弹性尺寸(.flexible())或自适应尺寸(.adaptive(minimum: 80))。

最后是 OutlineGroupList 的树形支持。OutlineGroup 可以递归地渲染嵌套数据结构(如文件系统),配合 Listchildren 参数可以创建可折叠的大纲视图。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 的动画系统
WWDC 2020