Hiding SwiftUI views

There are several ways to hide a SwiftUI view, although they don’t all agree on what it means to hide the view. Do you want it to be invisible, or actually not there?

To make it invisible you need only set its opacity to zero or use the hidden modifier. But the view will still be laid out just the same, and take up space in the GUI.

If you want to actually hide the view, so that it disappears completely, you can either not emit the view at all (e.g. conditionalise its existence with if or switch) or you can replace it with EmptyView.

EmptyView is pretty marvellous in this respect. Notice how even though it’s a real view – an actual value that you emit from a view builder – layout collections (e.g. HStack) don’t “see” it – there’s no visible gap, from doubling up on the cell padding, like there would be if they simply treated EmptyViews like 0 wide ⨉ 0 high views.

You might ask, when would you actually need EmptyView? Wouldn’t you’d just use conditional code to hide a view, e.g.:

struct MyView: View {
    let model: Model?
    
    var body: some View {
        if let model {
            Text(model.text)
        }
    }
}

// Now you can just use MyView(model: someOptional),
// without having to unwrap it before every use.

The above is making use of the “ViewBuilder mode”, to implicitly yield zero or more views which are automagically aggregated into a TupleView (if you emit more than one), an optional view (if you dynamically emit zero or one), etc. That’s very convenient, in simple cases like that.

But, “ViewBuilder mode” comes with a lot of limitations, including:

  • Compiler diagnostics are far inferior to regular Swift code.
  • You cannot return early (i.e. use return statements) except by throwing an exception, which is not always semantically appropriate.
  • You cannot have nested functions.
  • You sometimes cannot use full Swift control flow syntax (e.g. switch statements).

Fortunately, you can opt out of “ViewBuilder mode” by simply using an explicit return statement, but then you have to follow the usual rules for opaque return types, i.e. that the value be of the same type for every return statement. You can use AnyView in concert with EmptyView to straighten the compiler’s knickers regarding the return type1, e.g.:

struct MyView: View {
    let model: Model?
    
    var body: some View {
        guard let model else {
            return AnyView(EmptyView())
        }
        
        return AnyView(<actual view contents>)
    }
}

…or – if you don’t mind the return type being Optional, which SwiftUI itself doesn’t – you can use a partially opaque return type:

struct MyView: View {
    let model: Model?
    
    var body: Optional<some View> {
        guard let model else {
            return Body.none
        }
        
        return <actual view contents>
    }
}

Thanks to Dima Galimzianov for helping me figure out how to do that.

Note that in the trivial cases shown above there’s no particularly compelling reason to use these forms instead of the “ViewBuilder mode”, but you could hopefully imagine how, as the conditional logic gets more complicated, it becomes increasingly impractical to avoid using guard, or early returns otherwise.

Note that while AnyView has a lot of FUD2 associated with it, as far as I can tell there’s really nothing wrong with it. Most often it’s claimed to cause performance problems, but I’ve never detected that in my use, nor seen anyone provide a real example case of such. And multiple people have gone looking for performance problems with AnyView and found nothing. e.g. Alexey Naumov‘s Performance Battle: AnyView vs Group.

In case you’re wondering, there’s no difference in show/hide behaviour either – it just works!

It’s possible that there’s some situations in which using EmptyView might cause animation issues, by confusing SwiftUI as to what the relationship is between view hierarchies over time, but I haven’t found that to be the case in practice. And if it ever does crop up, you can always just explicitly id your views.

EmptyView is one of those little sleeper features that seems irrelevant until you stumble upon a need for it, and then it’s really nice to have.

  1. There’s technically another option: use a concrete return type instead. But, that’s usually impractical – SwiftUI uses opaque return types a lot, such as for virtually all view modifiers, which force you to use opaque return types in turn. And even when that isn’t an issue, good luck deducing the correct fully-qualified type name even for something as simple as a LabeledContent. ↩︎
  2. Literally, “Fear, Uncertainty, and Doubt”. Used here in that face-value sense, not to suggest any malice on anyone’s part. ↩︎

6 thoughts on “Hiding SwiftUI views”

  1. Note that use of AnyView is required in order to accomodate the SwiftUI View protocol requirement that body always return the same type of view.

    This is generally not necessary. The body property is implicitly annotated with @ViewBuilder (through the View protocol). So the usual ViewBuilder rules apply, i.e. you don‘t have to use AnyView nor does your if let require an else branch.

    Both of these variants work too:

        var body: some View {
            if let model {
                Text("Hello")
            } else {
                EmptyView()
            }
        }

        var body: some View {
            if let model {
                Text("Hello")
            }
        }

    For your own properties or functions, you can also mark them @ViewBuilder to get the same result. Example:

        @ViewBuilder var myHelperView: some View {
            if let model {
                Text("Hello")
            }
        }

    Reply
    • It’s true, in the highly simplified example I showed here you don’t need to use AnyView. I’ll revise it a bit to better show a situation where you do. Thanks for your feedback on this – it’s an important point and I’m glad to improve this post’s clarity.

      I’m not sure of the exact mechanics, but you can opt-out of “ViewBuilder” mode, intentionally or not. e.g. if you explicit return. And I find that’s sometimes unavoidable (in a pragmatic sense – you can always contrive some amount of conditional logic that will effectively mimic early returns, but it can get ugly and IMO isn’t worth the trade-off merely to avoid AnyView – especially given the lack of any apparent real downside to AnyView in terms of performance.

      Reply
    • Post updated. I stopped short of providing a “real” example of where AnyView(EmptyView()) makes obvious sense, as the view body needs to be pretty non-trivial before you reach that point. But, I did try to spell out fundamentally why you might end up in that situation. Hopefully it all makes sense now. Let me know if anything remains unclear.

      Reply
  2. Nice writeup! Consider adding a comment about when to use opacity vs. hidden? For example, opacity(shouldHide ? 0 : 1) can be used to conditionally hide a view, while you’d need an if/else and recreate the view in each clause to conditionally hide with hidden. There may be other reasons I’m not aware of.

    Reply
    • Yeah, it’s a bit tangential to the point I was focused on – using EmptyView – but I did spend a little time trying to figure out what the hidden modifier actually does. i.e. does it just do opacity(0)? The way Apple talks about it, it sounds like that’s actually the case. But they never actually state it unequivocally. So I’m not certain what the difference might be; thus why I didn’t recommend one over the other.

      Unfortunately, I can’t find the implementation of the hidden modifier. It doesn’t seem to be part of the SwiftUI framework – no symbol entry for it. So maybe it’s always inlined? But I couldn’t find a source definition of it either, in the Xcode SDKs.

      Reply

Leave a Comment