Display a View when rest of content is empty

Viewed 72

I have a view body with logic such as this:

var body: some View {
  VStack {
    if someCondition {
      SomeView()
    }
    if anotherCondition {
      AnotherView()
      AnotherView()
    }
    if thirdCondition {
      SomeView()
      AnotherView()
    }
  }
}

This works fine to conditionally show elements vertically stacked. However, if none of the conditions are satisfied, the VStack is empty and my UI looks broken. I would like to show a placeholder instead. My current solution is to add a manual check at the end on !someCondition && !anotherCondition && !thirdCondition:

var body: some View {
  VStack {
    if someCondition {
      SomeView()
    }
    if anotherCondition {
      AnotherView()
      AnotherView()
    }
    if thirdCondition {
      SomeView()
      AnotherView()
    }
    if !someCondition && !anotherCondition && !thirdCondition { // 
      Text("Please select an element.")
    }
  }
}

However, this is difficult to keep the condition in sync with the content above. I was hoping there was some sort of view modifier I could use such as:

var body: some View {
  VStack {
    if someCondition {
      SomeView()
    }
    if anotherCondition {
      AnotherView()
      AnotherView()
    }
    if thirdCondition {
      SomeView()
      AnotherView()
    }
  }.emptyState { // 
    Text("Please select an element.")
  }
}

The closest thing I could find is this tutorial, but that requires passing in the condition as well.

Is there a way to build a view modifier like this emptyState which doesn't require duplicating the condition logic?


I was thinking I could use a ZStack for this:

var body: some View {
  ZStack {  // 
    // empty state text
    Text("Please select an element.")

    VStack {
      if someCondition {
        SomeView()
      }
      if anotherCondition {
        AnotherView()
        AnotherView()
      }
      if thirdCondition {
        SomeView()
        AnotherView()
      }
    }    
  }
}

... but then I run into a different issue where if I'm showing real content (e.g. SomeView()) but it's not large enough, I could see both SomeView() and the empty state text.

3 Answers

Here's one implementation using GeometryReader & it's named emptyState:

extension View {
    func emptyState<Content: View>(@ViewBuilder content: () -> Content) -> some View {
        return self.modifier(EmptyStateModifier(placeHolder: content()))
    }
}

struct EmptyStateModifier<PlaceHolder: View>: ViewModifier {
    @State var isEmpty = false
    let placeHolder: PlaceHolder
    func body(content: Content) -> some View {
        ZStack {
            if isEmpty {//Thanks to @Asperi
                placeHolder
            }
            content
                .background(
                    GeometryReader { reader in
                        Color.clear
                            .onChange(of: reader.frame(in: .global).size == .zero) { newValue in 
                                isEmpty = reader.frame(in: .global).size == .zero 
                            }
                    }
                )
        }
    }
}

If it is a long chaining condition, you can handle it with switch{}, then use the benefit of default to display the placeholder when 0 condition is met(stack is empty or no selection)

@State var selected = ""
var body: some View {
    VStack {
        switch selected {
        case "a":
            SomeView()
        case "b":
            AnotherView()
        case "c":
            ThirdView()
        //this default will show up
        //when there is no selection
        //and when the stack is empty meaning that all the above
        //conditions did not meet
        default:
            Text("Please select an element")
        }
    }
}

There is no straightforward, officially supported way of telling what the return value of a view builder contains.

It is more sensible to handle this at the model layer than in your view. Each of your conditions are part of your model. These should be wrapped up into a single value type, and you can use the presence or absence of that (or an internal calculated value of that, depending on your requirements) to inform what to put in the stack. For example:

struct Model {
    let one: Bool
    let two: Bool
    let three: Bool
}

struct MyView: View {
    let model: Model?

    var body: some View {
        VStack {
            switch model {
            case .some(let model):
                if model.one {
                    Text("One")
                }
                if model.two {
                    Text("Two")
                }
                if model.three {
                    Text("Three")
                }
            case .none:
                Text("Empty")
            }
        }
    }
}

(I'm assuming here that there is no valid Model which doesn't contain any of the values, that would be when it is set to nil)

Related