Weekly Swift articles, podcasts and tips by John Sundell.

Optional SwiftUI views

Published on 18 Feb 2020
Basics article available: SwiftUI

Sometimes we might want one of our SwiftUI views to only be constructed and shown in case a certain optional value is available. For example, here we’re building a HomeView that should conditionally contain a ProfileView whenever a LogInManager contains a loggedInUser — which we’ve tried implementing using a standard if let statement:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

    var body: some View {
        VStack {
            if let user = loginManager.loggedInUser {
                ProfileView(user: user)
            }

            ...
        }
    }
}

Unfortunately, the above code will give us a compiler error:

Closure containing control flow statement cannot be used with function builder ViewBuilder.

Since SwiftUI (for the most part) doesn’t use standard closures, but rather function builders, we can’t put any arbitrary code within the closures used to configure views like HStack and VStack. So how can we handle optionals like the one above?

One way would be to push the responsibility of handling such optionals into the views that actually consume them. For example, here’s how we could make our ProfileView accept an optional User, rather than a concrete value:

struct ProfileView: View {
    var user: User?

    var body: some View {
        guard let user = user else {
            // We have to use 'AnyView' to perform type erasure here,
            // in order to give our 'body' a single return type:
            return AnyView(EmptyView())
        }

        return AnyView(VStack {
            Text(user.name)
            ...
        })
    }
}

The above works, but isn’t very elegant. After all, it doesn’t make much sense to create a ProfileView for a nil user. So let’s try another approach instead, for example by using map on our optional User, in order to conditionally convert it into a ProfileView — like this:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

    var body: some View {
        VStack {
            loginManager.loggedInUser.map { user in
                ProfileView(user: user)
            }
            ...
        }
    }
}

To learn more about the above way of mapping an optional value into a new type, check out the Basics article about optionals.

That’s much nicer, as we no longer have to manually construct an EmptyView in case our User value is missing — and it also makes it possible for us to again have ProfileView accept a concrete User, rather than an optional. But perhaps we could take things even further?

The cool thing about SwiftUI’s @ViewBuilder function builder is that it isn’t a private implementation detail of SwiftUI itself, but rather a public attribute that we can annotate our own functions and closures with as well.

Using that attribute, we could construct an Unwrap view — that’ll accept an optional value, and a @ViewBuilder closure for transforming any non-nil value into a View — like this:

struct Unwrap<Value, Content: View>: View {
    private let value: Value?
    private let contentProvider: (Value) -> Content

    init(_ value: Value?,
         @ViewBuilder content: @escaping (Value) -> Content) {
        self.value = value
        self.contentProvider = content
    }

    var body: some View {
        value.map(contentProvider)
    }
}

With the above in place, we can now not only make our optional-unwrapping UI code read nicer, but we can also make full use of SwiftUI’s DSL when constructing our optional views — since we’re using the same @ViewBuilder functionality that SwiftUI itself uses. For example, we could now easily construct an entire optional view hierarchy like this:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

    var body: some View {
        VStack {
            Unwrap(loginManager.loggedInUser) { user in
                HStack {
                    Text("Logged in as:")
                    ProfileView(user: user)
                }
            }
            ...
        }
    }
}

We could also do something similar for modifiers as well, but let’s save that for a future article, shall we? 😀