Highlight
SwiftUI 新增了
ForEach(subviewOf:)、Group(subviewsOf:)、ForEach(sectionOf:)和ContainerValuesAPI,让开发者能够构建支持任意内容组合、分组和自定义修饰符的容器视图。
核心内容
SwiftUI 的 List 已经支持灵活的内容组合——可以混用静态视图和 ForEach 动态生成的视图,还能用 Section 进行分组,甚至通过 listRowSeparator() 这样的修饰符自定义行样式。但如果你想构建自己的容器视图(比如卡片网格、看板布局),过去只能依赖数据驱动的初始化方式,灵活性受限。
苹果在今年推出了三个新 API 来解决这个问题。ForEach(subviewOf:) 可以遍历任意视图的「已解析子视图」——无论这些子视图是静态声明的还是通过 ForEach 动态生成的。Group(subviewsOf:) 则一次性获取所有已解析子视图的集合,适合需要统计数量或批量处理的场景。ForEach(sectionOf:) 用于检测和遍历视图中的 Section,让自定义容器也能像 List 一样支持分组。配合 ContainerValues,容器可以读取子视图上设置的自定义值,实现类似 listRowSeparator() 的容器特定修饰符。
这套 API 的关键在于理解「声明视图」和「已解析视图」的区别。声明视图是你写在代码里的 ViewBuilder 内容,已解析视图是 SwiftUI 在运行时实际渲染的视图——ForEach 会根据数据展开成多个视图,if 语句会根据条件决定是否渲染。新 API 操作的都是已解析视图,这让容器实现更加简洁。
详细内容
基础容器实现
一个数据驱动的 DisplayBoard 容器(05:27):
struct DisplayBoard<Data: Collection, Content: View>: View {
var data: Data
@ViewBuilder var content: (Data.Element) -> Content
var body: some View {
DisplayBoardCardLayout {
ForEach(data) { item in
CardView {
content(item)
}
}
}
.background { BoardBackgroundView() }
}
}
关键点:
data集合通过@ViewBuilder闭包映射为视图ForEach迭代数据并生成CardView- 容器只支持这种单一的数据驱动模式
支持任意内容组合
重构为使用 ViewBuilder 和 ForEach(subviewOf:)(05:27):
struct DisplayBoard<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
DisplayBoardCardLayout {
ForEach(subviewOf: content) { subview in
CardView {
subview
}
}
}
.background { BoardBackgroundView() }
}
}
// 使用示例
DisplayBoard {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body like View")
ForEach(songsFromSam) { song in
Text(song.title)
}
}
关键点:
- 移除数据驱动参数,改为单一的
@ViewBuilder content ForEach(subviewOf:)遍历「已解析子视图」,无论静态还是动态生成- 容器现在可以混用静态视图和 ForEach,无需修改实现
声明视图 vs 已解析视图
理解 SwiftUI 的视图解析过程(08:00):
// 1 个声明视图
ForEach(songsFromSam) { song in
Text(song.title)
}
// 9 个已解析子视图
Text("I Container Multitudes")
...
Text("Love Stack")
关键点:
- ForEach 是声明视图,没有视觉外观
- 它的作用是根据数据生成多个已解析子视图
- Group 会直接展开其内容为已解析子视图
- EmptyView 解析为零个子视图
- if 语句根据条件决定解析结果
批量处理子视图
使用 Group(subviewsOf:) 统计子视图数量(10:00):
var body: some View {
DisplayBoardCardLayout {
Group(subviewsOf: content) { subviews in
ForEach(subviews) { subview in
CardView(
scale: subviews.count > 15 ? .small : .normal
) {
subview
}
}
}
}
.background { BoardBackgroundView() }
}
关键点:
Group(subviewsOf:)获取已解析子视图的集合- 可以通过
count等属性访问集合信息 - 适合需要根据子视图数量动态调整布局的场景
支持 Section 分组
使用 ForEach(sectionOf:) 遍历 Section(11:42):
var body: some View {
HStack(spacing: 80) {
ForEach(sectionOf: content) { section in
VStack(spacing: 20) {
if !section.header.isEmpty {
DisplayBoardSectionHeaderCard { section.header }
}
DisplayBoardSectionContent {
section.content
}
.background { BoardSectionBackgroundView() }
}
}
}
.background { BoardBackgroundView() }
}
关键点:
ForEach(sectionOf:)检测并遍历内容中的 Section- 每个 section 有
header和content属性 header.isEmpty检查是否有头部内容- 容器可以为每个 Section 创建独立的布局
自定义容器修饰符
使用 ContainerValues 定义容器特定修饰符(14:46):
extension ContainerValues {
@Entry var isDisplayBoardCardRejected: Bool = false
}
extension View {
func displayBoardCardRejected(_ isRejected: Bool) -> some View {
containerValue(\.isDisplayBoardCardRejected, isRejected)
}
}
关键点:
ContainerValues是新的键值存储类型@Entry宏用于声明新的容器值条目containerValue()修饰符用于设置值
在容器中读取容器值(15:42):
struct DisplayBoardSectionContent<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
DisplayBoardCardLayout {
Group(subviewsOf: content) { subviews in
ForEach(subviews) { subview in
let values = subview.containerValues
CardView(
scale: (subviews.count > 15) ? .small : .normal,
isRejected: values.isDisplayBoardCardRejected
) {
subview
}
}
}
}
}
}
关键点:
- 子视图有
containerValues属性可读取 - Section 也有
containerValues - 应用于 Section 的修饰符会影响其所有子视图
使用自定义修饰符(16:15):
DisplayBoard {
Section("Matt's Favorites") {
Text("Scrolling in the Deep")
.displayBoardCardRejected(true)
Text("Born to Build & Run")
Text("Some Body like View")
}
Section("Sam's Favorites") {
ForEach(songsFromSam) { song in
Text(song.title)
.displayBoardCardRejected(song.samHasDibs)
}
}
}
关键点:
- 修饰符可以应用于单个视图
- 也可以应用于 ForEach 中的每个视图
- 应用于 Section 会影响所有子视图
核心启发
-
构建卡片网格容器:使用
ForEach(subviewOf:)构建类似 DisplayBoard 的卡片网格容器,支持静态卡片和动态卡片混合排列。这比手动解析 ViewBuilder 输出更可靠,也能更好地适应 SwiftUI 的声明式更新机制。先从简单的网格布局开始,验证子视图遍历逻辑正确后再扩展功能。 -
实现看板布局的列分组:使用
ForEach(sectionOf:)为看板布局(Kanban board)添加列分组支持,每个 Section 对应一列,Section header 作为列标题。这种方式比自己定义数据模型来表示列更直观,用户可以直接用 SwiftUI 的 Section API 来组织内容,降低使用成本。 -
为容器添加装饰性修饰符:使用
ContainerValues为容器视图添加特定的装饰选项,比如网格项的跨列设置、卡片的强调样式、或者是否禁用某些交互。这类修饰符与容器强相关,不适合用 Environment 或 Preference 实现,而ContainerValues正是为这种场景设计的——值只对直接容器可见,不会泄漏到视图层级中。
关联 Session
- What’s new in SwiftUI — SwiftUI 每年的核心更新汇总
- Advanced SwiftUI containers — 更深入的容器构建技巧
- Meet SwiftUI for spatial computing — 空间计算中的 SwiftUI 容器应用
- Build accessible apps with SwiftUI — 构建无障碍应用时的容器考虑
评论
GitHub Issues · utterances