Weekly Swift articles, podcasts and tips by John Sundell.

Adding SwiftUI’s ViewBuilder attribute to functions

Published on 30 Jul 2020
Basics article available: SwiftUI

The 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.