SwiftUI - The If Statement's Sharp Edges

SwiftUI - The If Statement's Sharp Edges


Xcode error message: Closure containing control flow statement cannot be used with Function Builder 'ViewBuilder'
Looking for the tl;dr? Skip down to the Workarounds section below.


Summary

In working with the SwiftUI domain specific language (DSL), you will see some familiar syntax like forEach and if <boolean condition> { }. In the SwiftUI DSL, Apple is leveraging already understood syntax like this to provide some continuity between day-to-day Swift code and the SwiftUI DSL — This is great until it isn’t 😅.

You may notice some odd behavior where your Swift code doesn’t quite work the same in SwiftUI. For example, if you are using an if statement with a simple boolean check to optionally show a view, everything works just fine. If you were to take the conditional check one step further to something familiar like evaluating an enum or to use the if let syntax to conditionally unwrap an optional, you will be left scratching your head. This is exactly what happened to me, and this post is about exploring the sharp edges around SwiftUI’s use of the if statement.

Note: At the time of this writing, SwiftUI is still in beta and changing a…lot… This post is meant to highlight some of the edge cases, workarounds, and to help contribute to the collective understanding about this new and exciting technology.


What’s the deal with the if statement

A little background

If you pass SwiftUI an optional view, and it happens to be nil, SwiftUI will not render that view. This is useful if you only want a view to be visible sometimes.

You can make use of this in the body that produces a SwiftUI view by using a simple if statement. What SwiftUI will do is take the if statement and produce optional views that are nil or not nil depending on the conditional logic.

Screenshot showing a different SwiftUI view when tapped

struct MaybeDuckView: View {
    @State  var showDuck: Bool = true

    var body: some View {
        VStack {
            // SwiftUI if statement that will conditionally show one of these views
            if showDuck {
                Text("🦆")
            } else {
                Text("🐘")
            }
            // Using the same boolean to change the string
            Text(showDuck ? "Quack" : "Quack?")
                .font(.system(size: 24))
        }
        .font(.system(size: 40))
        .gesture(TapGesture().onEnded { _ in
            self.showDuck.toggle()
        })
    }
}

Side note: One strength of the if statement is that if SwiftUI is listening to the conditional statement (e.g., via the @State property wrapper, a binding, etc.), SwiftUI will reevaluate that if statement to choose the right view to show when the state changes.

The problem

What if we want to use other forms of the if statement like if let or if case? Sadly, they don’t work 😕.

SwiftUI builds views using the ViewBuilder struct, which leverages a new Swift feature called Function Builders.


Aside: To learn more about Function Builders:


The implementation of ViewBuilder includes support for if statements, but as seen in the below screenshot there is only support for the basic if statement control flows (i.e., if, if/else, if/else if).

Looking at the statement that reads, “…that is visible only when the if condition evaluates true…“, tells us that the if statement condition needs to be a boolean result, which sadly doesn’t include the other if statement variations.

For example, we cannot use the if case style or the if let styles (switch statements won’t work either).

This will not compile ❌:

enum FeatureState {
    case active
    case disabled
}

struct FeatureView: View {
    @State var featureState: FeatureState

    var body: some View {
        VStack {
            // Will not compile. Error message:
            // Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
            if case .active = featureState {
                Text("Active view!")
            } else {
                Text("Disabled view")
            }
        }
    }
}

Note: the VStack is used as a container to guarantee to the compiler that a view will be returned.

In digging more into this, I came across this interesting statement from the Function Builder draft proposal (seen at the bottom of the proposal), which addresses this limitation. Seems like Function Builders will — by design — provide support for a more general buildOptional, and leave the specifics, like supporting different flavors of if statements, up to the implementations:

The function-building methods in this proposal have been deliberately chosen to cover the different situations in which results can be produced rather than to try to closely match the different source structures that can give rise to these situations. For example, there is a buildOptional that works with an optional result, which might be optional for many different reasons, rather than a buildIf that takes a condition and the result of applying the transformation to the controlled block of the if…it is not be possible to come up with a finite set of such rules that could be soundly applied to an arbitrary function in Swift. For example, there would be no way to to apply that buildIf to an if let condition: for one, the condition isn’t just a boolean expression, but more importantly, the controlled block cannot be evaluated before the condition has been (uniquely and successfully) evaluated to bind the let variable. We could perhaps add a buildIfLet to handle this, but the same idea could never be extended to allow a buildIfCase or buildReturn.

And although these other types of control flow are not currently supported in SwiftUI, we will hopefully see better parity between SwiftUI DSL control flow and the Swift language control flow in the future; As noted by Apple in Swift forums, which is promising 💪:

…The future SwiftUI DSL will be the current SwiftUI DSL but without as many restrictions, and it will deploy backwards, so remembering the restrictions (and differences like buildOptional vs. buildIf) will be pointless.

Workarounds

If you can’t wait, like me, for SwiftUI control flow to gain additional functionality, we have some options.


Thoughts


Feel free to reach out on Twitter — cheers!

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora