Articles, podcasts and news about Swift development, by John Sundell.

Using multiple computed properties to form a SwiftUI view’s body

Published on 29 Dec 2020
Discover page available: SwiftUI

One of SwiftUI’s most powerful traits is just how composable its views are by default. Because of that, breaking up a massive view can often simply be a matter of moving pieces of its body code into new View types, and then wiring up any bindings or other kinds of actions as needed.

However, there are also multiple other techniques that can be good to keep in mind when we’d like to break up a larger view in order to improve its maintainability and readability. To take a look at one such technique, let’s start with the following ProfileView, which currently uses a single body property to form its internal view hierarchy:

struct ProfileView: View {
    var user: User

    var body: some View {
        ZStack {
            LinearGradient(
                gradient: user.profileGradient,
                startPoint: .top,
                endPoint: .bottom
            )
            .edgesIgnoringSafeArea(.all)

            ScrollView {
                VStack(spacing: 15) {
                    Text(user.name)
                        .font(.title)
                    Text(user.biography)
                        .multilineTextAlignment(.leading)
                }
                .padding()
                .foregroundColor(.white)
            }
        }
    }
}

While the above is not really a massive view in terms of line count, it could definitely be broken up in order to make it easier to iterate on and expand its functionality over time. One way to do that would be to use the aforementioned approach of splitting its body property up into separate View types — which in this case could be implemented as private nested types in order to give our code a neat degree of namespacing:

private extension ProfileView {
    struct BackgroundView: View {
        var gradient: Gradient

        var body: some View {
            LinearGradient(
                gradient: gradient,
                startPoint: .top,
                endPoint: .bottom
            )
            .edgesIgnoringSafeArea(.all)
        }
    }

    struct InfoView: View {
        var user: User

        var body: some View {
            VStack(spacing: 15) {
                Text(user.name)
                    .font(.title)
                Text(user.biography)
                    .multilineTextAlignment(.leading)
            }
            .padding()
            .foregroundColor(.white)
        }
    }
}

With the above in place, we’ll now be able to drastically simplify our main ProfileView, since we can now implement its body just by instantiating our BackgroundView and InfoView within their respective ZStack and ScrollView wrappers:

struct ProfileView: View {
    var user: User

    var body: some View {
        ZStack {
            BackgroundView(gradient: user.profileGradient)
ScrollView {
    InfoView(user: user)
}
        }
    }
}

However, while the above approach is definitely a great option in many cases, it sort of feels a bit “heavy” in this kind of situation — given that we both need to define multiple types just to split our code up, and that we need to pass parts of our model data down into those nested views, which can quickly end up feeling like unnecessary busywork.

So let’s explore another approach, which involves creating additional body-like computed properties for the different parts of our view’s internal hierarchy. Since we have a quite natural separation between our view’s foreground and background in this case, let’s name our properties just like that, and if we use the same some View opaque return type as SwiftUI’s built-in body property does, then we’ll end up with the following implementation:

private extension ProfileView {
    var background: some View {
        LinearGradient(
            gradient: user.profileGradient,
            startPoint: .top,
            endPoint: .bottom
        )
        .edgesIgnoringSafeArea(.all)
    }

    var foreground: some View {
        ScrollView {
            VStack(spacing: 15) {
                Text(user.name)
                    .font(.title)
                Text(user.biography)
                    .multilineTextAlignment(.leading)
            }
            .padding()
            .foregroundColor(.white)
        }
    }
}

With that change in place, all that we now have to do to implement our main view is to combine our background and foreground using a ZStack — which gives us a body implementation that essentially acts as a “handoff point” between our private implementation and SwiftUI as a framework:

struct ProfileView: View {
    var user: User

    var body: some View {
        ZStack {
            background
foreground
        }
    }
}

The beauty of the above approach is that it doesn’t require us to pass any additional data around, since our entire implementation can now actually remain within a single type — while still achieving a very nice separation between our view’s various parts.

Of course, it’s still a good idea to split certain views up into completely separate types (especially if we want to make some of those parts easier to reuse), but the above technique can be great to keep in mind when we want to break up a larger view while still letting its implementation continue to act as a single unit.