
概述
在 WWDC 24 新推出的 SwiftUI 6.0 中,苹果对于容器内部子视图的布局有了更深入的支持。为了能够未雨绸缪满足实际 App 中所有可能的情况,我们还可以再接再厉,将 Sections 的支持也考虑进去。

SwiftUI 6.0 对容器子视图布局的增强支持可以认为是一个小巧的容器自定义布局(Custome Layout)简化器。
在本篇博文中,您将学到如下内容:
- SwiftUI 6.0 容器内容的遍历和重新组合
- SwiftUI 6.0 中新的容器子视图布局考量
- 容器子视图的遍历
- 容器子视图的重新组合
有了全新容器子视图布局机制的加持,现在对于任何需要适配自定义容器行为的情况我们都可以游刃有余、从容应对了!
那还等什么呢?Let's go!!!;)
1. SwiftUI 6.0 容器内容的遍历和重新组合
苹果在 SwiftUI 4.0(iOS 16)中推出了自定义容器布局(Custom Layout)功能,有了它我们即可放心大胆的创建具有独特外观和行为的容器了:

Layout 协议弥补了 SwiftUI 不能自定义容器布局之遗憾,将容器中子视图渲染位置的自由度发挥到了极致。
不过,有时候我们仅仅希望稍微调整一下现有容器子视图的布局,比如换成系统默认的现成容器(VStack、HStack、ZStack 等)。在这种情况下使用自定义容器布局 Layout 略显大材小用了。
于是乎,在 SwiftUI 6.0(iOS 18)中苹果给了我们另一套"组合拳":容器内容的遍历(ForEach)和再组合(Group)。
2. SwiftUI 6.0 中新的容器子视图布局考量
简单来说,在 SwiftUI 6.0 中我们可以使用 ForEach 和 Group 的新构造器来实现容器内容的遍历(探囊取物)和重新组合(聚沙成塔)。前者可以帮助我们方便的将 @ViewBuilder 传入的内容分解为单个容器的子元素,而后者则能让我们在容器整体布局的重构上一展拳脚。
在 SwiftUI 6.0 之前,如果我们想实现一个自定义的 Card 容器可能会这样做:
swift
struct Card<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
VStack {
content
}
.padding()
.background(Material.regular, in: .rect(cornerRadius: 8))
.shadow(radius: 4)
}
}
然后这样来使用它:
swift
struct ContentView: View {
var body: some View {
Card {
Text("Hello, World!")
Text("My name is Majid Jabrayilov")
}
}
}
在上面的代码中,我们利用泛型结构将实际的容器内容通过 @ViewBuilder 语法块传递给了 Card 主体。但是,我们无法再更进一步去获取传入容器内部的子元素了,这意味着此时对容器子元素外观的细粒度定制无异于"敲冰求火"。
当然,我们可以通过其它手段来间接达到获取和区分容器内部子元素之目的,但这会使得代码逻辑变得根牙盘错、晦涩难懂。
3. 容器子视图的遍历
上面这种尴尬局面在 SwiftUI 6.0 中立刻变得烟消云散了,这得益于 ForEach 全新的 ForEach(subviews:) 构造器。

我们现在可以从容的遍历容器内部的每个独立元素视图了:
swift
struct Carousel<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(subviews: content) { subview in
subview
.containerRelativeFrame(.horizontal)
.clipShape(RoundedRectangle(cornerRadius: 15))
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.contentMargins(16)
}
}
struct ContentView: View {
var body: some View {
Carousel {
Color.yellow
Color.orange
Color.red
Color.blue
Color.green
}
.frame(height: 200)
}
}
在上面的代码中,我们通过 ForEach 的新构造器将传入 Carousel 容器内容的每个子视图都"抽取"出来单独应用外观样式。
注意 ForEach 新构造器闭包中传入实参的类型是 SubView 结构,它同时遵守 View 和 Identifiable 协议,这意味着我们可以把它当做一个普通且"特立独行"的 SwiftUI 视图来渲染。

上面 Carousel 容器的显示效果如下所示:

上面貌似看起来没什么大不了的,不过利用 ForEach 新构造器实现自定义容器的神奇才刚刚开始。现在我们可以恣意向 Carousel 传递任意"异构"的子视图啦:
swift
struct ContentView: View {
var body: some View {
Carousel {
Color.yellow
Text("Hello World")
Color.red
Toggle("成为黑悟空", isOn: .constant(true))
.tint(.pink)
.padding(.horizontal)
Color.green
}
.font(.largeTitle.bold())
.frame(height: 200)
}
}
编译并在 Xcode 预览中即可立见分晓:

4. 容器子视图的重新组合
虽然遍历容器内容"很好很强大",但在某些场景中与其遍历每个单独的容器元素,我们更希望先获得它们的一个整体(集合)然后再按需求重新组织它们的布局。
这时,我们可以利用 Group 的新构造器来优雅的完成这一功能:

从上面的定义中可以看到:Group(subviews:) 闭包中传入的实参类型是 SubviewsCollection。

从它遵循以下几个协议可以看出,它是一个集合,并且每个元素类型都是 SubView:
- BidirectionalCollection
- Collection
- Copyable
- RandomAccessCollection
- Sequence
- View
Group 新构造器的作用和 ForEach 的正相反:前者是"聚沙成塔",后者则是"拆塔成沙"。
在下面的代码中,我们创建了一个名为 Magazine 的新容器,并用 Group 新的构造器按照实际子视图的数量完成了自定义容器布局:
swift
struct Magazine<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
ScrollView {
Group(subviews: content) { subviews in
if !subviews.isEmpty {
subviews[0]
.padding(.horizontal)
.containerRelativeFrame(.vertical) { length, _ in
return length / 3
}
}
if subviews.count > 1 {
ScrollView(.horizontal) {
LazyHStack {
ForEach(subviews[1...], id: \.id) { subview in
subview
.containerRelativeFrame([.horizontal, .vertical]) { length, type in
switch type {
case .horizontal:
length
case .vertical:
200
}
}
.clipShape(.rect(cornerRadius: 15))
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.contentMargins(16)
}
}
}
}
}
在 Magazine 的实现中,我们还利用 containerRelativeFrame 修改器方法将容器内部各个子视图的尺寸设置为特定的大小(相对容器本身或自定义值)。

还用之前那个 ContentView 一试身手吧:
swift
struct ContentView: View {
var body: some View {
Carousel {
Color.yellow
Text("Hello World")
Color.red
Toggle("成为黑悟空", isOn: .constant(true))
.tint(.pink)
.padding(.horizontal)
Color.green
}
.font(.largeTitle.bold())
.frame(height: 200)
}
}
运行可以发现,现在我们容器中第一个子视图被置顶突出显示,剩余所有的子视图横列滚动显示:

在下一篇博文中,我们将继续自定义布局的探寻之旅,来学习如何让它们支持 SwiftUI 中的 Sections。
总结
在本篇博文中,我们介绍了 SwiftUI 6.0(iOS 18)中对自定义容器布局的增强支持,使我们能够自如做到"探囊取物"和"聚沙成塔"。
感谢观赏,再会吧!8-)