Optional SwiftUI views
Discover page available: SwiftUISometimes 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? 😀
Support Swift by Sundell by checking out this sponsor:

Bitrise: My favorite continuous integration service. Automatically build, test and distribute your app on every Pull Request — which lets you quickly get feedback on each change that you make. Start 2021 with solid continuous integration from Bitrise, using hundreds of ready-to-use steps.