Adding SwiftUI’s ViewBuilder attribute to functions
Discover page available: SwiftUIThe ViewBuilder
function builder attribute plays a very central role within SwiftUI’s DSL, and is what enables us to combine and compose multiple views within containers like HStack
and VStack
by simply creating instances of those views.
That attribute can also come very much in handy when we wish to extract certain parts of a given view’s body
into dedicated functions. As an example, let’s say that we’re working on a SongRow
view that renders a Song
model, along with a button that enables the user to either play or pause that 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 that we’re planning to add a few new features to the above view, but before doing so, we’d like to refactor its body
a bit as to prevent it from growing too much in complexity. For example, rather than constructing our button’s label
inline, we could move that logic to a private utility method, which might end up looking like this:
private extension SongRow {
func makeButtonLabel() -> some View {
if isPlaying {
return AnyView(PauseIcon())
} else {
return AnyView(PlayIcon())
}
}
}
However, while implementing certain expressions and conditions as separate methods can be a great way to increase the overall readability of our code, the above implementation has a quite significant downside compared to when that logic was inlined within our view’s body
.
Since our new method uses two separate types of views (either PauseIcon
or PlayIcon
), we need to use AnyView
to perform type erasure in order to give both of our code branches the same return type. Or do we?
It turns out that we can actually apply the same ViewBuilder
attribute that SwiftUI itself uses to our own methods as well — which lets us remove the use of AnyView
, by enabling us to simply type out our view expressions just like we can when placing those expressions within a SwiftUI closure:
private extension SongRow {
@ViewBuilder func makeButtonLabel() -> some View {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
}
With the above in place, we can now simply call makeButtonLabel()
when constructing our Button
, like this:
struct SongRow: View {
...
var body: some View {
HStack {
...
Button(
action: { self.isPlaying.toggle() },
label: { makeButtonLabel() }
)
}
}
}
Another option that’s also worth considering in the above kind of situation is to implement parts of our UI as separate View
types instead — for example like this in the case of our playback button:
struct PlaybackButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(
action: { self.isPlaying.toggle() },
label: {
if isPlaying {
PauseIcon()
} else {
PlayIcon()
}
}
)
}
}
We could then use our new, specialized button within our main 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)
}
}
}
My general rule of thumb: When I’m just looking to make a given view’s body
easier to read by extracting certain parts of its logic, then I’ll use a private @ViewBuilder
method — but when I want to transform a piece of a view into a more generally reusable component, then I’ll create a new View
type.