iosswiftuivstackgeometryreaderswiftui-scrollview

Views overlapping in VStack


I have some data that will be loaded from a server and in the mean time I want to show a shimmer placeholder view.

To achieve the shimmer effect, I've created the following:

struct ShimmerEffectBox: View {
    @State private var startPoint: UnitPoint = .init(x: -1.8, y: -1.2)
    @State private var endPoint: UnitPoint = .init(x: 0, y: -0.2)
    
    private var gradientColors = [
        Color(uiColor: .systemGray5),
        Color(uiColor: .systemGray6),
        Color(uiColor: .systemGray5)
    ]
    
    var body: some View {
        LinearGradient(colors: gradientColors,
                       startPoint: startPoint,
                       endPoint: endPoint)
            .onAppear {
                withAnimation(.easeInOut(duration: 2)
                    .repeatForever(autoreverses: false)) {
                        startPoint = .init(x: 1, y: 1)
                        endPoint = .init(x: 2.2, y: 2.2)
                }
            }
    }
}

The shimmer works fine and nothing needs to be looked at here.

I then create a ScrollView with some data and my placeholder views, however, for some reason the views are overlapping, this is my code:

struct ContentView: View {
    var body: some View {
        ScrollView {
            Text("Title")
                .font(.system(size: 20))
                .bold()
            
            ForEach(1...5, id: \.self) { _ in
                placeholderView
            }
        }
    }
    
    private var placeholderView: some View {
        GeometryReader { proxy in
            VStack(alignment: .leading, spacing: 4) {
                ShimmerEffectBox()
                    .frame(width: proxy.size.width / 2, height: 18)
                    .cornerRadius(4)
                
                ShimmerEffectBox()
                    .frame(height: 16)
                    .cornerRadius(4)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(.bottom, 24)
        }
    }
}

This gives me the following result:

VStack SwiftUI overlapping views

I'm sure that I'm doing something wrong with the geometry reader, so I added the padding to the geometry reader

private var placeholderView: some View {
    GeometryReader { proxy in
        VStack(alignment: .leading, spacing: 4) {
            ShimmerEffectBox()
                .frame(width: proxy.size.width / 2, height: 18)
                .cornerRadius(4)
            
            ShimmerEffectBox()
                .frame(height: 16)
                .cornerRadius(4)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }
    .padding(.bottom, 24)
}

SwiftUI VStack Overlapping

This is better, however, the padding is definitely not 24.

My goal is:

Not sure if I'm using geometry reader wrong or specifying the padding / spacing at the wrong place.


Solution

  • This problem is happening because the GeometryReader is inside a ScrollView, which stops it from being as greedy as usual. Consequently, the height of the GeometryReader is very small.

    Since you know the height of the ShimmerEffectBox (because you are setting it using .frame), you could fix the problem by setting a fixed height on the GeometryReader too:

    GeometryReader { proxy in
        // content as before
    }
    .frame(height: 38)
    

    However, it looks like you only need the GeometryReader to find the screen width, which you are dividing by two. The same result can be achieved by using an HStack that contains two views with maxWidth: .infinity. So you could also fix like this:

    private var placeholderView: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(spacing: 0) {
                ShimmerEffectBox()
                    .frame(height: 18)
                    .cornerRadius(4)
                    .frame(maxWidth: .infinity)
                Color.clear
                    .frame(maxWidth: .infinity)
            }
            ShimmerEffectBox()
                .frame(height: 16)
                .cornerRadius(4)
                .frame(maxWidth: .infinity)
        }
        .padding(.bottom, 24)
    }
    

    Animation

    If you want to fine-tune the spacing between the content of the ScrollView then you might want to use a VStack as the top-level container inside the ScrollView. Then you can control the spacing of the VStack:

    ScrollView {
        VStack(spacing: 0) { // or spacing: 24
            // content as before
        }
    }
    

    At the moment, the ScrollView is adding a bit of vertical space between each of the subviews it contains, which you might not be expecting.