深入理解 SwiftUI 的可变容器 View
SwiftUI 的可变容器 View 有VStack, HStack, ZStack, ForEach, Group等。
@ViewBuilder 因为是 @resultBuilder修饰的 struct,定义了很多静态方法buildBlock,这些方法可以接收一个或多个子 View,实际返回的是一个TupleView类型,最多接收 10 个参数。
我们可以用具体的方法来调用:
// TupleView<(Text, Text)>let inner = ViewBuilder.buildBlock(
Text("First"), Text("Second"))
// TupleView<(TupleView<(Text, Text)>, Text)>let outer = ViewBuilder.buildBlock( inner, Text("Third"))
outer.border(.blue)上面的每个Text都会包含一个蓝色的边框。进一步探索,我们把上面的outer放在List里面:
List { outer}结果就是每个Text变成 TableView 的经典 cell 样式。将outer换成Group包装,效果也会一样,会拆开每一个Text。但是VStack封装这 3 个Text的时候,结果就变成一个单独的 cell。
很显然,有些 View 是可以被拆解的,像 TupleView 和 Group 一样。所以有两个问题需要搞清楚:
- 怎么像
List一样修改 View 的每一个独立子 View? - 怎么实现一个像
Group和TupleView的 View?
再次深入到 SwiftUI 里面,会发现有些带下划线的隐藏 API ,比如用于打印状态变化的_printChanges() (opens in a new window):
struct ViewBuilderDemo: View { @State var count: Int = 0 var body: some View { if #available(iOS 15.0, *) { let _ = Self._printChanges() } else { // Fallback on earlier versions }
Text("Count: \(count)") Button { count += 1 } label: { Image(systemName: "square.and.arrow.up") .foregroundColor(.blue) } }}每次 Button 按下时,count +1,然后 console 打印:
ViewBuilderDemo: _count changed.这对调试 View 的时候非常有用。
在我们深入到VStack的源码时,发现了 _VariadicView以及VStack的核心_VStackLayout:
@frozenpublic struct VStack<Content> : View where Content : View { @usableFromInline internal var _tree: _VariadicView.Tree<_VStackLayout, Content>
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) { _tree = .init( root: _VStackLayout(alignment: alignment, spacing: spacing), content: content()) }
public typealias Body = Swift.Never}接下来看看_VariadicView.Tree:
public enum _VariadicView { @frozen public struct Tree<Root, Content> where Root : _VariadicView_Root {
public var root: Root
public var content: Content
@inlinable internal init(root: Root, content: Content) { self.root = root self.content = content }
@inlinable public init(_ root: Root, @ViewBuilder content: () -> Content) { self.root = root self.content = content() } }}_VariadicView.Tree的参数是_VariadicView_Root以及Content的类型。
从前面可知Content是遵守View协议的,所以_VariadictView.Tree也肯定是一样的。
extension _VariadicView.Tree : View where Root : _VariadicView_ViewRoot, Content : View {}这个协议看起来像这样:
public protocol _VariadicView_ViewRoot : _VariadicView_Root { associatedtype Body : SwiftUI.View
@ViewBuilder func body(children: _VariadicView.Children) -> Body}如果仔细点,可以发现这个有点像ButtonStyle以及类似的协议。并且_VariadicView.Children顾名思义就是一系列子 View 的集合。
总结一下:
_VariadicView.Tree初始化需要root和ViewBuilder的返回值Root将一系列子 View 包装成一个 View- 如果
Tree的Content遵守View协议,那Tree自身也遵守View协议
我们来写一个List和 VStack风格的容器 View,给每个子 View 之间加上 Divider。
首先,我们用@ViewBuilder来初始化,然后 body 使用 _VariadicView.Children 实现,带上DividedVStackLayout:
struct DividedVStack<Content: View>: View { var content: Content
init(@ViewBuilder content: () -> Content) { self.content = content() }
var body: some View { _VariadicView.Tree(DividedVStackLayout()) { content } }}
struct DividedVStackLayout: _VariadicView_UnaryViewRoot { @ViewBuilder func body(children: _VariadicView.Children) -> some View { children }}_VariadicView.Children 还遵循协议RandomAccessCollection,它的Element不仅是View还是Identifiable。如此,可以使用 _VariadicView.Children 来代替 ForEach。在每一个 element 之后增加 Divider,布局使用VStack。
当然,最后一个Divider还得忽略掉。我们可以记录一下最后的 element 的 id,ForEach 的时候判断一下即可。
代码如下:
struct DividedVStackLayout: _VariadicView_UnaryViewRoot { @ViewBuilder func body(children: _VariadicView.Children) -> some View { let last = children.last?.id
VStack { ForEach(children) { child in child
if child.id != last { Divider() } } } }}当我们使用的时候,就可以输入好几个 View 了:
DividedVStack { Text("First") Text("Second") Text("Third")}
但这里有一个问题,DividedVStack在使用 modifier 或 container 时候是把它当做一个整体来看待的,就像VStack一样。
DividedVStack { Text("First") Text("Second") Text("Third")}.border(.blue)
这里的蓝色边框是包在最外面的,并没有对每一个Text添加 border。这就是_VariadicView_UnaryViewRoot在起作用了,如果有一个Tree,它的Root遵守这个协议,它就会被当作单个的 View,而不是一组 View 的集合。与之对应的就是_VariadicView_MultiViewRoot协议,只要遵守了这个协议,我们就可以分开DividedVStack里面的每一个子 View 了。
struct Divided<Content: View>: View { /* … */
var body: some View { _VariadicView.Tree(DividedLayout()) { content } }}
struct DividedLayout: _VariadicView_MultiViewRoot { @ViewBuilder func body(children: _VariadicView.Children) -> some View { let last = children.last?.id
ForEach(children) { child in child
if child.id != last { Divider() } } }}写一个 demo 试试效果:
HStack { Divided { Text("First") Text("Second") Text("Third") } .border(.blue)}
Swift 是一个多范式的编程语言,除了基本的面向对象这种范式,更多的是面向 Protocol 编程。尤其是在写 SwiftUI 的时候,Protocol 贯穿了整个设计理念。
SwiftUI 的 View 是 struct 类型的,都是 immutable 的,无法使用 class 的继承方式,更做不到类似 Flutter 的 InheritedWidget 那样共享数据。
SwiftUI 布局的核心就是那些容器 View,通过深入挖掘 SDK 里面的隐藏 API,能够帮助我们更好的了解其实现细节,有助于我们自定义容器 View。