Adding SwiftUI’s @ViewBuilder
Attribute to Functions
SwiftUI’s ViewBuilder
function builder attribute is a central component of SwiftUI's DSL (Domain-Specific Language). It allows us to combine and compose multiple views within containers like HStack
and VStack
easily. However, you can also leverage this attribute when you want to extract certain parts of a view's body into dedicated functions, improving code readability and maintainability.
Let’s explore this concept with an example. Suppose you’re working on a SongRow
view that displays a Song
model along with a button to play or pause the song:
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()
}
}
)
}
}
}
Now, let’s say you want to add more features to this view, and you want to refactor its body to prevent it from becoming too complex. For instance, you might want to move the logic for constructing the button’s label to a private utility method:
private extension SongRow {
func makeButtonLabel() -> some View {
if isPlaying {
return AnyView(PauseIcon())
} else {
return AnyView(PlayIcon())
}
}
}
However, using AnyView
for type erasure is not the most elegant solution. The good news is that you can apply the ViewBuilder
attribute to your own methods, just like SwiftUI does. This allows you to return different types of views without using AnyView
:
private extension SongRow {
@ViewBuilder func makeButtonLabel() -> some View {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
}
With this change, you can now call makeButtonLabel()
directly when constructing your button, like this:
struct SongRow: View {
...
var body: some View {
HStack {
...
Button(
action: { self.isPlaying.toggle() },
label: { makeButtonLabel() }
)
}
}
}
This approach improves code readability and eliminates the need for AnyView
.
Another option to consider, especially for more reusable components, is to create separate view types. In the case of your playback button, you can define it as a distinct view:
struct PlaybackButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(
action: { self.isPlaying.toggle() },
label: {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
)
}
}
Then, you can use the PlaybackButton
within your SongRow
view by passing a binding reference to its isPlaying
property:
struct SongRow: View {
var song: Song
@Binding var isPlaying: Bool
var body: some View {
HStack {
...
PlaybackButton(isPlaying: $isPlaying)
}
}
}
In summary, SwiftUI’s ViewBuilder
attribute is a versatile tool that allows you to improve code organization and readability by extracting view logic into separate functions. Whether you choose to use private @ViewBuilder
methods or create new view types depends on your specific requirements, but both approaches contribute to more maintainable SwiftUI code.