ViewBuilder函数builder属性在SwiftUI的DSL中扮演着非常重要的角色,它使我们能够通过简单地创建视图的实例来在容器(如HStack和VStack)中组合和结合多个视图。
当我们希望将给定视图主体的某些部分提取为专用函数时,这个属性也非常方便。 举个例子,假设我们正在处理一个SongRow视图,它呈现一个歌曲模型,以及一个允许用户播放或暂停该歌曲的按钮
struct SongRow: View {
var song: Song
@Binding var isPlaying: Bool
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(song.name).bold()
Text(song.artist.name)
}
Spacer()
Button(
action: { self.isPlaying.toggle() },
label: {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
)
}
}
}
现在,假设我们计划向上面的视图添加一些新特性,但在此之前,我们想对它的主体进行一些重构,以防止它变得过于复杂。例如,我们可以将这个逻辑移到一个私有的实用方法中,而不是将按钮的标签构造为内联的,最终可能会像这样:
private extension SongRow {
func makeButtonLabel() -> some View {
if isPlaying {
return AnyView(PauseIcon())
} else {
return AnyView(PlayIcon())
}
}
}
然而,虽然将某些表达式和条件作为单独的方法来实现是提高代码整体可读性的好方法,但与将逻辑内联到视图体相比,上面的实现有相当大的缺点
由于我们的新方法使用两种不同类型的视图(PauseIcon或PlayIcon),我们需要使用AnyView来执行类型擦除,以便为我们的两个代码分支提供相同的返回类型,
实际上,我们也可以将SwiftUI本身使用的ViewBuilder属性应用到我们自己的方法上, 这使得我们可以通过简单地输入视图表达式,就像我们在swift闭包中输入这些表达式一样,最终删除AnyView的使用
private extension SongRow {
@ViewBuilder func makeButtonLabel() -> some View {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
}
有了上面的步骤,我们现在可以在构造按钮时简单地调用makeButtonLabel(),如下所示:
struct SongRow: View {
...
var body: some View {
HStack {
...
Button(
action: { self.isPlaying.toggle() },
label: { makeButtonLabel() }
)
}
}
}
在上述情况下,另一个值得考虑的选项是将我们的UI的某些部分作为单独的视图类型来实现——例如回放按钮的例子
struct PlaybackButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(
action: { self.isPlaying.toggle() },
label: {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
)
}
}
然后,我们可以在我们的主SongRow视图中使用我们新的,专门的按钮,通过传递一个绑定引用到它的isPlaying属性:
struct SongRow: View {
var song: Song
@Binding var isPlaying: Bool
var body: some View {
HStack {
...
PlaybackButton(isPlaying: $isPlaying)
}
}
}
我的一般经验法则是:当我只是希望通过提取逻辑的某些部分来使给定视图的主体更容易阅读时,那么我将使用私有的@ViewBuilder方法- 但当我想把一个视图的一部分转换成一个更普遍可重用的组件时,我会创建一个新的视图类型