4

The issue with Conditional View Modifiers

I made heavy use of conditional view modifiers in SwiftUI until I had some issues with it and recently discovered it is a bad idea.

From my understanding now, doing something like this:

if condition {
    view
} else {
    view.hidden()
}

means that SwiftUI will treat view in both cases as completely different views. So if you jump from if to else or vice versa, view is recreated, including running onAppear and any initial animations. Besides that, it also breaks animating view from one branch into the other. Even adding .id() doesn't seem to solve this.

I attached a minimum viable example so you can see this behaviour for yourself. And needless to say, this is bad. It has major performance implications, breaks animations and is even worse if you perform some heavy operation (like a network request) in your onAppear.

The solution?

The solution appears to be to do something like this:

view.padding(condition? 10 : 0)

Everything works peachy, SwiftUI keeps view around, great.

The problem: .isHidden()

Now, I need to conditionally hide a view. And to be honest, I have no idea how to achieve it. .isHidden() has no parameter to enable or disable it, so there is no way to use the same approach as shown above.

So: How can I conditionally hide a view without recreating it? The reason I don't want to recreate the view is that onAppear gets called again and it reset the state of the view, triggering animations to the initial state again. Or am I using the wrong approach entirely in SwiftUI world here?

BlackWolf
  • 5,239
  • 5
  • 33
  • 60

3 Answers3

2

A possible approach is to use opacity+disabled, like

view
  .opacity(isHidden ? 0 : 1)
  .disabled(isHidden ? true : false)
Asperi
  • 228,894
  • 20
  • 464
  • 690
1

you can put a SwiftUI View into UIViewController and use UIViewControllerRepresentable to present it in SwiftUI code. This way you get to save the View's states while also removing it from view hierarchy. Here's a minimal example, you can scroll the scroll view and click the button to hide it and show it again, and the scroll view stays in the same offset without getting recreated. Hope it helps.

import SwiftUI


let myView = MyView()
let hostingController = UIHostingController(rootView: myView)


struct MyView: View {
    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading) {
                ForEach(0 ... 100, id: \.self) {i in
                    Text("\(i)")
                }
            }
        }
        
    }
}

struct VC_Wrapper: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return hostingController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
    typealias UIViewControllerType = UIViewController
}

struct ContentView: View {
    @State var isShow = true {
        didSet {
            hostingController.view.isHidden = !isShow
        }
    }
    var body: some View {
        ZStack {
            VC_Wrapper()
            Button("show/hide") {
                isShow.toggle()
            }.background(.red)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Yanpei Shi
  • 11
  • 3
0

In SwiftUI a View struct is just data, SwiftUI is what diffs the data and decides what actual UIViews to add, remove, update on screen. So if you want to hide something just give it empty data, e.g.

Text(showText ? "Some text" : "")

It would be helpful to know what data it is you are trying to show/hide.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • I don't think this solves the issue, since setting the text to `""` would change the size of `Text` - I am using `.hidden()` though because I want my view to participate in layouting, but not be visible to the user. I am implementing a reusable loading/error behaviour that can be applied to any view. If the view has an error loading data, it should be replaced by an error message (but the error message should have the same size as the original view) - therefore, I want to apply `.hidden()` to the view and show the error on top. So basically, all kinds of complex data are possible. – BlackWolf Jul 08 '22 at 13:57
  • It's best not to generalise View structs, they are lightweight data structures just make all the different kinds you need. – malhal Jul 08 '22 at 14:39
  • This logic lives in a view modifier, which observes a network request and updates the view accordingly (and shows either view or an error). Since this is more about behaviour than ui making as many as I need doesn’t really work I guess, as it would mean reimplementing the logic over and over. But besides that, the general issue (I have to hide a view conditionally) stays the same, no matter where I implement it – BlackWolf Jul 08 '22 at 17:17