Adding SwiftUI’s @ViewBuilder Attribute to Functions

KD Knowledge Diet
3 min readJun 20, 2024

--

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.

--

--

KD Knowledge Diet

Software Engineer, Mobile Developer living in Seoul. I hate people using difficult words. Why not using simple words? Keep It Simple Stupid!