WWDC Quick Look 💓 By SwiftGGTeam
Stacks, Grids, and Outlines in SwiftUI

Stacks, Grids, and Outlines in SwiftUI

观看原视频

Highlight

SwiftUI 2020 新增 LazyVStackLazyHStackLazyVGridLazyHGridList(children:)OutlineGroupDisclosureGroup,让长列表、网格布局、层级数据和检查器控件都能用组合式视图直接实现。


核心内容

Cody 先讲了一个三明治图库。最初的实现很自然:把 HeroView 放进 ForEach,外面套一层 VStack,再用 ScrollView 负责滚动。数据少的时候没有问题;照片变多之后,页面打开时要先创建和测量所有图片,首屏响应变慢,内存也跟着涨。

SwiftUI 2020 的答案是 lazy stack。外层滚动容器换成 LazyVStack 后,首屏只渲染可见的三明治卡片,后面的内容等用户滚动到附近再创建。内部的评分 HStack 和图片覆盖层不用改成 lazy,因为一个 HeroView 出现在屏幕上时,里面的内容本来就要同时显示。

接着,同一个图库被搬到 iPad 上。单列图片在大屏上浪费空间,所以 session 引入 LazyVGridLazyHGrid。开发者用 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 新增 LazyVStackLazyHStack,让滚动方向上的内容按需渲染。

// 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 明确建议:不确定时先用 VStackHStack,用 Instruments 找到性能瓶颈后再采用 lazy stack。

Lazy grid:大屏上提高内容密度

05:48)三明治图库放到 iPad 后,单列布局只是把图片变大,没有增加屏幕上的内容数量。LazyVGridLazyHGridGridItem 描述网格尺寸,让同一批数据可以按列或行排布。

// 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) 读取列配置,并按可用宽度生成尽可能多的等宽列。
  • ForEachHeroView 没有变,布局容器承担从列表到网格的变化。
  • 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:52List(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
  • ToggleColorRow 是被折叠或展开的实际控件。
  • Label("Fill", …) 组合标题和图标,transcript 中使用的是 SF Symbol 图标。

核心启发

  • 长内容图库:做一个照片、菜单或商品图库,首版可以先用 ScrollViewLazyVStack。开始时保留现有 cell 视图,只替换外层 VStack,再用 Instruments 检查首屏响应和内存。

  • 自适应 iPad 网格:把手机上的单列内容搬到 iPad 或 macOS 窗口时,用 LazyVGridGridItem(.adaptive(minimum: 300))。从一个最小列宽开始调,避免为横屏、竖屏和窗口宽度分别写多套布局。

  • 文档侧边栏树:如果 App 有文件夹、图层、画布对象或任务分组,可以让模型暴露 children,再用 List(items, children: \.children) 做可展开 sidebar。单行视图保持简单,树遍历交给 SwiftUI。

  • 分区式大纲编辑器:当数据有多个根集合,例如多个 canvas、多个项目或多个账户,使用 ListForEachSectionOutlineGroup 组合。这样每个分区都有自己的标题,每个分区内部仍能展开树。

  • 紧凑检查器面板:把不常用的表单控件放进 DisclosureGroup。常用组用 isExpanded binding 默认展开,其他组保持折叠,适合属性面板、设置页和编辑器侧栏。

关联 Session

评论

GitHub Issues · utterances