iosswiftuiswiftui-zstackswiftui-vstack

Creating a Stack that have the combined behavior of VStack and ZStack


I want to create a custom class for a stack that will show some kind of stacked cards effect like so:

Stacked cards

It will be used on top of the Main / Root container. The stack will be aligned at the bottom of the Main / Root container. The width will be the same as the parent container, while the height will be dynamic based on the content of the StackingContainer. I've tried using VStack but the content will be separate StackingContainer cards with the bottom border still showing + there will be spacing between the cards. If I use the ZStack, I don't know how to dynamically resize the height of the stack and the StackingContainers. The content will always be blocked by the card above it.

How can I achieve that?


Solution

  • So I have fixed the issue. This is how I solved it.

    At first, I used the usual VStack with rounded corner elements like this:

    public struct RoundedStackContainer<Content: View>: View {
        
        private let content: () -> Content
        
        public init(content: @escaping () -> Content) {
            self.content = content
        }
        
        public var body: some View {
            ZStack {
                Color.white
                    .cornerRadius(20, corners: [.topLeft, .topRight])
                    .clipShape(RoundedRectangle(cornerRadius: 20))
                    .shadow(radius: 10)
                content()
                    .padding(.all, 20)
            }
        }
    }
    
    #Preview {
        VStack {
            RoundedStackContainer {
                VStack {
                    Text("First container")
                    Spacer()
                    Image(systemName: "bolt.fill")
                        .font(.system(size: 64))
                    Spacer()
                }
            }
            RoundedStackContainer {
                VStack {
                    Text("Second container")
                    Spacer()
                    Image(systemName: "person.crop.circle")
                        .font(.system(size: 64))
                    Spacer()
                }
            }
            RoundedStackContainer {
                VStack {
                    Text("Third container")
                    Spacer()
                    Image(systemName: "globe")
                        .font(.system(size: 64))
                    Spacer()
                }
            }
        }
    }
    

    And it looked like this which is not what I had in mind (and thus the question): Default rounded corner container

    Then I just drop the padding down 40 px like so:

    public struct RoundedStackContainer<Content: View>: View {
        
        private let content: () -> Content
        
        public init(content: @escaping () -> Content) {
            self.content = content
        }
        
        public var body: some View {
            ZStack {
                Color.white
                    .cornerRadius(20, corners: [.topLeft, .topRight])
                    .clipShape(RoundedRectangle(cornerRadius: 20))
                    .shadow(radius: 10)
                content()
                    .padding(.horizontal, 20)
                    .padding(.top, 20)
                    .padding(.bottom, 60) //Adjust the internal element padding
            }
            .padding(.bottom, -40) // Drop the padidng down 40 px so the whole bottom part is behind the next one
        }
    }
    
    #Preview {
        VStack {
            RoundedStackContainer {
                VStack {
                    Text("First container start")
                    Spacer()
                    Image(systemName: "bolt.fill")
                        .font(.system(size: 64))
                    Spacer()
                    Text("First container end")
                }
            }
            RoundedStackContainer {
                VStack {
                    Text("Second container start")
                    Spacer()
                    Image(systemName: "person.crop.circle")
                        .font(.system(size: 64))
                    Spacer()
                    Text("Second container end")
                }
            }
            RoundedStackContainer {
                VStack {
                    Text("Third container start")
                    Spacer()
                    Image(systemName: "globe")
                        .font(.system(size: 64))
                    Spacer()
                    Text("Third container end")
                }
            }
        }
    }
    

    And voila! It works:

    Stacking containers

    Additional answer: This is working and all but now I want to simplify using a parent container stack that will just wrap the whole thing automatically so now I added this:

    public struct RoundedStack<Content: View>: View {
        
        @ViewBuilder public let content: () -> Content
        
        public var body: some View {
            ExtractMulti(content) { subviews in // From View Extractor - https://github.com/GeorgeElsham/ViewExtractor
                VStack {
                    ForEach(subviews) { subview in
                        RoundedStackContainer {
                            subview
                        }
                    }
                }
            }
        }
    }
    

    And we use it like so:

    #Preview {
        RoundedStack {
            VStack {
                Text("First container start")
                Spacer()
                Image(systemName: "bolt.fill")
                    .font(.system(size: 64))
                Spacer()
                Text("First container end")
            }
            VStack {
                Text("Second container start")
                Spacer()
                Image(systemName: "person.crop.circle")
                    .font(.system(size: 64))
                Spacer()
                Text("Second container end")
            }
            VStack {
                Text("Third container start")
                Spacer()
                Image(systemName: "globe")
                    .font(.system(size: 64))
                Spacer()
                Text("Third container end")
            }
        }
    }