swiftuiz-indexhierarchical

Set zIndex of Views in different View-Hierarchy


I have 4 Views: A, B, C and D

A and B are inside the VStack C.

C and D both are in a ZStack.

I want A to be beneath B, therefore both are in a VStack.

The problem is, that I want to set the zIndex of A to 1, B to 3 and D to 2. The zIndex only applies to Views inside the same View-Hierarchy. But if I put A, B and D into the same ZStack, then A and B are no longer below each other. If I put D inside the VStack C, then it is not on top of A and B any more...

How can I solve this puzzle?

I want to achieve this, because in my case, D is an overlay that should detect click events on everything but on B (which is a pop-up-menu). A is a button to show the menu B. Clicks on D are supposed to close the menu B. But the items in B should still be clickable.

Here is an example:

struct Test: View {
    @State private var isMenuVisible = false
    
    var body: some View {
        ZStack {
            VStack { //C
                Button(action: { //A
                    isMenuVisible.toggle()
                }, label: {
                    Text("Open Menu")
                })
                
                if (isMenuVisible) {
                    Rectangle() //B
                        .fill(Color.blue)
                        .overlay {
                            Button(action: {
                                print("is clickable!")
                            }, label: {
                                Text("Menu-Button")
                            })
                        }
                }
                
                Spacer()
                
            }
            
            if (isMenuVisible) {
                Color.red // D
                    .opacity(0.6)
                    .onTapGesture {
                        isMenuVisible.toggle()
                    }
            }
        }
    }
}

If I put the if-Statement inside the ZStack, then it works, but then not the entire screen is covered.


Solution

  • 1. You can use this hierarchy instead:

    VStack  
      |---OptionButton
      |---ZStack
            |---Dismisser
            |---Menu
    

    2. If it's necessary to overlap on Option-Button:

    struct Test: View {
        @State private var isMenuVisible = false
        //@State private var dynamicHeight = 50.0
        
        var body: some View {
            ZStack(alignment: .top) {
                Color.clear // make ZStack wider
                
                Button {
                    isMenuVisible.toggle()
                } label: { 
                    Text("Open Menu")
                }
                .frame(height: 50)//dynamicHeight = observeHeight
                .border(.green, width: 1)
                
                if isMenuVisible {
                    Color.red //Dismisser
                        .opacity(0.5)
                        .onTapGesture {
                            isMenuVisible.toggle()
                        }
                    
                    VStack {
                        Button(action: {
                            print("is clickable 1!")
                        }, label: {
                            Text("Menu-Button-1")
                        })
                        
                        Button(action: {
                            print("is clickable 2!")
                        }, label: {
                            Text("Menu-Button-2")
                        })
                    }
                    .border(.green, width: 1)
                    .padding(.top, 50)//.padding(.top, dynamicHeight)
                }
            }
        }
    }
    

    for observing live high of any view

    .overlay(
        GeometryReader { geo in
            Color.clear.onAppear {
                dynamicHeight = geo.size.height
            }
            .onChange(of: geo.frame(in: .global)) { _ in
                dynamicHeight = geo.size.height
            }
        }
    )