@ViewBuilder
是一个属性包装器,也是一个自定义的函数包装器,用于构建一个或多个视图。在 SwiftUI
中,很多地方都使用了 @ViewBuilder
,例如 VStack
、HStack
、ZStack
和 Group
等。它可以接受多个视图并返回一个单一的组合视图。
比如我们最常用的View
协议中的body
,就是用了@ViewBuilder
,下面是body
的定义:
public protocol View {
associatedtype Body : View
@ViewBuilder @MainActor var body: Self.Body { get }
}
body
属性前已经用@ViewBuilder
进行了修饰,这样,我们在body
闭包里就可以添加一个或者多个视图,也不需要些return
这样的关键字,SwiftUI
将接受多个视图并返回一个单一的组合视图。
如果仿写一个类似View的协议,去掉body
前修饰的@ViewBuilder
:
protocol NonViewBuilderView {
associatedtype Body : View
@MainActor var body: Self.Body { get }
}
struct ViewBuilderDemo: NonViewBuilderView {
var body: some View {
Text("Hi")
}
}
我们创建界面ViewBuilderDemo
,遵守并实现NonViewBuilderView
协议,在body
中只放入了一个Text
文本,运行起来是没问题的。
但是如果在里面放入了不同类型的视图,就会报错了: Branches have mismatching types 'Text' and 'Button<Text>'
解决这个问题也比较简单,给body
加上@ViewBuilder
即可:
struct ViewBuilderDemo: NonViewBuilderView {
@State private var isText: Bool = false
@ViewBuilder
var body: some View {
if isText {
Text("Hi")
} else {
Button("Button") { }
}
}
}
@ViewBuilder修饰属性
在开发过程中,有时候为了让body
瘦身,会把里面很多的UI逻辑部分抽出来到一个属性当中去,比如下面的代码:
enum ViewType {
case one
case two
case three
}
struct ViewBuilderDemo: View {
var viewType: ViewType = .one
var body: some View {
contentView
}
@ViewBuilder
private var contentView: some View {
switch viewType {
case .one:
Text("One")
case .two:
HStack {
Text("Two")
Image(systemName: "heart.fill")
}
case .three:
Image(systemName: "heart.fill")
}
}
}
在ViewBuilderDemo
结构体中,我们定义了一个私有变量contentView
,这个变量返回了一些根据逻辑要显示的不同的视图。
如果这个contentView
属性不加@ViewBuilder
修饰,那么必然会报错了。
@ViewBuilder修饰方法
有些时候我们更喜欢用方法去封装一些UI,比如上面同样的逻辑,改成使用方法。
@ViewBuilder
private func contentViewBaseOnType() -> some View {
switch viewType {
case .one:
Text("One")
case .two:
HStack {
Text("Two")
Image(systemName: "heart.fill")
}
case .three:
Image(systemName: "heart.fill")
}
}
原理和修饰属性一样,方法里面也返回了很多类型的组件,一个或者多个,如果不用@ViewBuilder
修饰就会报错的。
@ViewBuilder修饰参数
有时候自定义组件,需要从外部传入具体的显示内容,而组件只是负责一些布局类的逻辑,比如下面这段代码:
struct HeaderView<Content: View>: View {
let isHorizontal: Bool
let content: Content
init(isHorizontal: Bool, content: () -> Content) {
self.isHorizontal = isHorizontal
self.content = content()
}
var body: some View {
if isHorizontal {
HStack {
content
}
} else {
VStack {
content
}
}
}
}
HeaderView
组件有两个属性,isHorizontal
和content
,根据isHorizontal
的不同,进而选择不同的排列方式将content
显示。这里面的Content
采用了泛型,继承了View
,如果直接定义一个View
类型的content
会报错的。
在init
方法中,传入isHorizontal
和构建content
的闭包,就目前的定义看似没有问题,但是在调用的时候报错了。
这里的错误主要是我们初始化方法里面构建content
的闭包有问题,这个闭包里面可以放置很多类型的组件,SwiftUI
认为这些组件组成的一个大组件不符合View协议。这就和我们上面说的很类似,我们需要告知SwiftUI
,构建一个组合视图,修改一下init方法即可,在content
参数前加上@ViewBuilder
。
init(isHorizontal: Bool, @ViewBuilder content: () -> Content) {
self.isHorizontal = isHorizontal
self.content = content()
}
除了这种方法,我们也可以省略init
方法,在定义content
属性的地方添加@ViewBuilder
。重构一下HeaderView
组件:
struct HeaderView<Content: View>: View {
let isHorizontal: Bool
@ViewBuilder let content: () -> Content
var body: some View {
if isHorizontal {
HStack {
content()
}
} else {
VStack {
content()
}
}
}
}
这里将content
直接定义为闭包类型,并且前面加上了@ViewBuilder
,这样在初始化的时候同样不会报错。
写在最后
@ViewBuilder
是 SwiftUI
中一个非常有用的工具,它极大地简化了视图的组合和复用。通过在属性,方法以及初始化方法上合理使用 @ViewBuilder
,可以更有效地构建复杂且高效的用户界面。
最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。