We form View in SwiftUI, proceeding from conditions

Sometimes we need to create a SwiftUI View, given some conditions. For example, in the above code, we define a HomeView , which may contain a ProfileView , if LogInManager has loggedInUser . We are trying to implement this using the standard if statement:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

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

            ...
        }
    }
}

Unfortunately, this code will throw an error when compiling:
Closure containing control flow statement cannot be used with function builder ViewBuilder.

Since function closers are not used here, but function builders , we cannot put arbitrary code into them to form an HStack or VStack. So how do we get out of this situation?

One way is to pass the processing of such optionals directly to the view that we are creating. For example, we can pass in our ProfileView not a specific User value, but make it optional:

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)
            ...
        })
    }
}

This code works, but not particularly beautiful. It makes no sense to create a ProfileView for user nil. Let's take a different approach: use map to our optional User to convert it to ProfileView:

struct HomeView: View {
    @ObservedObject var loginManager: LoginManager

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

This is already much prettier: we do not need to manually give EmptyView when User has no value. We can again pass a specific value to the ProfileView, not optional. Is it possible to do even better?

The good news about @ViewBuilder is that this is not some kind of private implementation in SwiftUI, but an accessible attribute with which we can annotate our own functions and closures.

Using this attribute, we can compile a view Unwrap, which takes an optional value as a parameter and a @ViewBuilder tagged closure to convert a non-nil value to View:

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)
    }
}

Using this design, we can now completely redesign the entire structure of the HomeView:

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)
                }
            }
            ...
        }
    }
} 

All Articles