Highlight
SwiftUI 2020 新增
LazyVStack、LazyHStack、LazyVGrid、LazyHGrid、List(children:)、OutlineGroup和DisclosureGroup,让长列表、网格布局、层级数据和检查器控件都能用组合式视图直接实现。
核心内容
Cody 先讲了一个三明治图库。最初的实现很自然:把 HeroView 放进 ForEach,外面套一层 VStack,再用 ScrollView 负责滚动。数据少的时候没有问题;照片变多之后,页面打开时要先创建和测量所有图片,首屏响应变慢,内存也跟着涨。
SwiftUI 2020 的答案是 lazy stack。外层滚动容器换成 LazyVStack 后,首屏只渲染可见的三明治卡片,后面的内容等用户滚动到附近再创建。内部的评分 HStack 和图片覆盖层不用改成 lazy,因为一个 HeroView 出现在屏幕上时,里面的内容本来就要同时显示。
接着,同一个图库被搬到 iPad 上。单列图片在大屏上浪费空间,所以 session 引入 LazyVGrid 和 LazyHGrid。开发者用 GridItem 描述列或行的尺寸,既可以固定三列,也可以用 .adaptive(minimum:) 让列数随可用宽度变化。
后半段换到 ShapeEdit,一个运行在 macOS、iPadOS 和 iOS 的文档型 App。它一开始只需要展示画布上的扁平图形列表;加入”图形组可以包含图形组”之后,数据变成任意深度的树。List(children:) 能直接沿着 children key path 展开树,OutlineGroup 则把同样的能力放进自定义列表结构里。
最后是检查器面板。填充、阴影、文本等控件全摊开时太拥挤,DisclosureGroup 让每组控件有标题、有展开状态、有内容区域;配合 Label 和布尔 binding,还能设置默认展开的组。这让 SwiftUI 的布局故事从”摆放视图”延伸到”逐步展示信息”。
详细内容
Lazy stack:滚动内容只在可见时创建
(03:27)普通 VStack 会在 ScrollView 出现时枚举所有三明治并创建对应视图。transcript 里的问题很具体:照片越多,页面越久才响应。Apple 新增 LazyVStack 和 LazyHStack,让滚动方向上的内容按需渲染。
// Fetch sandwiches from the sandwich store
let sandwiches: [Sandwich] = …
ScrollView {
LazyVStack(spacing: 0) {
ForEach(sandwiches) { sandwich in
HeroView(sandwich: sandwich)
}
}
}
关键点:
ScrollView仍然负责滚动;lazy stack 自己不提供滚动能力。LazyVStack(spacing: 0)替换原来的VStack(spacing: 0),这是性能变化的入口。ForEach(sandwiches)保持数据驱动写法,每个模型对应一个HeroView。- session 明确建议:不确定时先用
VStack或HStack,用 Instruments 找到性能瓶颈后再采用 lazy stack。
Lazy grid:大屏上提高内容密度
(05:48)三明治图库放到 iPad 后,单列布局只是把图片变大,没有增加屏幕上的内容数量。LazyVGrid 和 LazyHGrid 用 GridItem 描述网格尺寸,让同一批数据可以按列或行排布。
// Fetch sandwiches from the sandwich store
let sandwiches: [Sandwich] = …
// Define grid columns
var columns = [
GridItem(.adaptive(minimum: 300), spacing: 0)
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(sandwiches) { sandwich in
HeroView(sandwich: sandwich)
}
}
}
关键点:
GridItem(.adaptive(minimum: 300), spacing: 0)要求每列至少 300 点宽。LazyVGrid(columns: columns, spacing: 0)读取列配置,并按可用宽度生成尽可能多的等宽列。ForEach和HeroView没有变,布局容器承担从列表到网格的变化。- transcript 特别提到
.adaptive适合横屏 iPad 和可任意调整窗口大小的 macOS。
List children:把扁平列表变成层级列表
(08:41)ShapeEdit 的 sidebar 原本只是画布图形的列表。图形组可以继续包含子图形后,列表要表达树结构。SwiftUI 给 List 增加 children key path,用一处参数告诉列表如何遍历数据树。
struct GraphicsList: View {
var graphics: [Graphic]
var body: some View {
List(
graphics,
children: \.children
) { graphic in
GraphicRow(graphic)
}
.listStyle(SidebarListStyle())
}
}
关键点:
graphics是树的根节点数组。children: \.children告诉List每个Graphic的子节点在哪里。GraphicRow(graphic)仍然只负责单行展示,层级展开由List处理。SidebarListStyle()带来 sidebar 样式;transcript 提到 iOS 14 中的 bold headers 也来自这个方向的 list 样式改进。
OutlineGroup:在自定义结构里复用树遍历
(09:52)List(children:) 适合整个列表就是一棵树的情况。ShapeEdit 需要每个 canvas 一个 section,每个 section 里面再放一棵图形树,于是改用 OutlineGroup。
// Customizing your outlines
List {
ForEach(canvases) { canvas in
Section(header: Text(canvas.name)) {
OutlineGroup(canvas.graphics, children: \.children)
{ graphic in
GraphicRow(graphic)
}
}
}
}
关键点:
- 外层
List继续负责 selection、scrolling 和列表交互。 ForEach(canvases)为每个 canvas 创建一个分区。Section(header: Text(canvas.name))把 canvas 名称变成分区标题。OutlineGroup(canvas.graphics, children: \.children)在每个分区内部遍历树形图形数据。
DisclosureGroup:控制非树状信息的展开
(13:10)检查器里的 fill、shadow、text 控件不属于规则树结构,但它们也需要折叠。DisclosureGroup 提供展开指示器、label 和 content;如果传入布尔 binding,还能由状态控制默认展开。
// Progressive display of information
Form {
DisclosureGroup(isExpanded: $areFillControlsShowing) {
Toggle("Fill shape?", isOn: isFilled)
ColorRow("Fill color", color: fillColor)
} label: {
Label("Fill", …)
}
…
}
关键点:
Form适合承载设置和一组控件,session 也提到它可用于 macOS 的 Settings Scenes。DisclosureGroup(isExpanded:)把展开状态交给areFillControlsShowing。Toggle和ColorRow是被折叠或展开的实际控件。Label("Fill", …)组合标题和图标,transcript 中使用的是 SF Symbol 图标。
核心启发
-
长内容图库:做一个照片、菜单或商品图库,首版可以先用
ScrollView加LazyVStack。开始时保留现有 cell 视图,只替换外层VStack,再用 Instruments 检查首屏响应和内存。 -
自适应 iPad 网格:把手机上的单列内容搬到 iPad 或 macOS 窗口时,用
LazyVGrid加GridItem(.adaptive(minimum: 300))。从一个最小列宽开始调,避免为横屏、竖屏和窗口宽度分别写多套布局。 -
文档侧边栏树:如果 App 有文件夹、图层、画布对象或任务分组,可以让模型暴露
children,再用List(items, children: \.children)做可展开 sidebar。单行视图保持简单,树遍历交给 SwiftUI。 -
分区式大纲编辑器:当数据有多个根集合,例如多个 canvas、多个项目或多个账户,使用
List、ForEach、Section和OutlineGroup组合。这样每个分区都有自己的标题,每个分区内部仍能展开树。 -
紧凑检查器面板:把不常用的表单控件放进
DisclosureGroup。常用组用isExpandedbinding 默认展开,其他组保持折叠,适合属性面板、设置页和编辑器侧栏。
关联 Session
- What’s new in SwiftUI — SwiftUI 2020 总览,包含本 session 展开的 outlines、grids 和 controls。
- App essentials in SwiftUI — transcript 结尾点名的后续 session,继续讲 app、scene 和 Settings Scenes。
- Data Essentials in SwiftUI — transcript 结尾点名的后续 session,讲模型数据如何驱动视图更新。
- Build document-based apps in SwiftUI — ShapeEdit 是文档型 App 场景,这个 session 补足 DocumentGroup 和文档管理基础。
评论
GitHub Issues · utterances