Search code examples
swiftuioverlay

A perplexing phenomenon in SwiftUI:


As shown in the code below, my CardContentView consists of elements arranged vertically in a VStack. However, when I place it within an overlay, it behaves as if it were in a ZStack. Why is this so?

Screenshot

import SwiftUI

struct TestView: View {
    @Namespace var namespace
    @State var isShowDetail = false
    @State var selectedCardIndex: Int?
    
    var body: some View {
        VStack {
            Rectangle()
                .fill(.gray.opacity(0.05))
                .ignoresSafeArea()
        }
        .overlay {
            if let selectedCardIndex, isShowDetail {
                DetailView(selectedCardIndex)
                    .transition(.asymmetric(insertion: .identity, removal: .offset(y: 5)))
            }
        }
        .overlay {
            CardContentView()
        }
    }
    
    @ViewBuilder
    func TesttView(_ index: Int) -> some View {
        Rectangle()
            .frame(width: .infinity, height: 100)
            .foregroundColor(.red.opacity(0.3))
            .overlay {
                Text("\(index)")
            }
    }

    @ViewBuilder
    func DetailView(_ index: Int) -> some View {
        VStack {
            TesttView(index)
                .matchedGeometryEffect(id: index, in: namespace)
        }
    }

    @ViewBuilder
    func CardContentView() -> some View {
        VStack {
            ScrollView(.vertical, showsIndicators: false) {
                    ForEach(0..<10) { index in
                        VStack {
                            if index == selectedCardIndex && isShowDetail {
                                Rectangle()
                                    .fill(.clear)
                            } else {
                                TesttView(index)
                                    .matchedGeometryEffect(id: index, in: namespace)
                                    .offset(y: isShowDetail ? 1000 : 0)
                                    .rotation3DEffect(
                                        .init(degrees: 10),
                                        axis: (x: 1.0, y: 0.0, z: 0.0),
                                        anchor: .center,
                                        anchorZ: 0.0,
                                        perspective: 1.0
                                    )
                                    .onTapGesture {
                                        selectedCardIndex = index

                                        withAnimation(.easeInOut(duration: 5)) {
                                            isShowDetail.toggle()
                                        }
                                    }
                                    .padding()
                                    

                            }
                        }
                }
            }
        }
        .overlay {
            if let selectedCardIndex, isShowDetail {
                VStack {
                    HStack {
                        Image(systemName: "chevron.left")
                            .onTapGesture {
                                withAnimation {
                                    isShowDetail.toggle()
                                }
                            }

                        Spacer()

                        Text("Detail")
                            .font(.system(size: 20, weight: .bold))
                    }
                    DetailView(selectedCardIndex)
                    Spacer()
                }
            }
        }        
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

When I remove CardContentView from the overlay and place it in a VStack, it returns to normal.

var body: some View {
        VStack {
            CardContentView()
        }
        .overlay {
            if let selectedCardIndex, isShowDetail {
                DetailView(selectedCardIndex)
                    .transition(.asymmetric(insertion: .identity, removal: .offset(y: 5)))
            }
        }
        .overlay {
//            CardContentView()
        }
    }

Screenshot


Solution

  • Your CardContentView behaves as if it were in a ZStack when placed within an overlay because of how SwiftUI handles the layout and layering of views.

    When you use the overlay modifier, it overlays the content on top of the base view. However, in your case, you are overlaying multiple views on top of each other within the VStack. SwiftUI uses a ZStack internally to handle this overlaying of views. This is why you observe behavior similar to a ZStack.

    When you remove the CardContentView from the overlay and place it in a VStack, the views are arranged one after another as expected because VStack stacks views vertically.