SwiftUI - The If Statement's Sharp Edges
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.
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:
- Swift evolution draft proposal to add Function Builders to the Swift Language.
- Swift forums discussion of Function Builders and the draft proposal.
- Blog post by John Sundell about SwiftUI features that power SwiftUI’s API, that includes details about Function Builders (The post notes that these are all Swift 5.1 features, but Function Builders may not end up being part of Swift 5.1).
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).
-
If you press
cmd + shift + o
in the Xcode 11 beta and type inViewBuilder
, you can see the definition for theViewBuilder
struct.
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 ❌:
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.
- Pull the boolean producing logic into a function or computer property (continuing the example from above):
struct FeatureView: View { @State var featureState: FeatureState private func shouldShowDetailView(for state: FeatureState) -> Bool { switch state { case .active: return true case .disabled: return false } } var body: some View { VStack { if shouldShowDetailView(for: featureState) { Text("Active view!") } else { Text("Disabled view") } } } }
- Use a function or computed property to return the view you want to conditionally show:
struct FeatureView: View { ... private var featureDetailView: some View { switch featureState { case .active: return Text("Active view!") case .disabled: return Text("Disabled view") } } var body: some View { featureDetailView } }
- As Majid pointed out on on Twitter, you can also leverage
map
on optionals instead ofif let
.
Thoughts
- One of the more subtle, and tricky, things about SwiftUI, which also is one of the strengths of the SwiftUI DSL, is the continuity in syntax between the Swift language and SwiftUI DSL. The thing to keep in mind, is that even if the syntax is the same, don’t just assume that all the behavior is the same.
- Another example is
forEach
. You can read more aboutforEach
in this Hacking With Swift Post.
- Another example is
- Although SwiftUI has its youthful sharp edges, embracing SwiftUI for what it is today, including the head-scratching moments, can yield a lot of great learning opportunities like this one; all while setting us up to be highly productive as SwiftUI matures.
- Overall, I am excited about the future of SwiftUI!
Feel free to reach out on Twitter — cheers!