Search code examples
iosswiftuiscrollview

Adjusting scrollview gives error with offset


I am using scrollview for my topCardView and bottomView. When I try to scroll everytime it gives me an error of invalid frame dimensions or sometimes it get freeze. Bound preference SizePreferenceKey tried to update multiple times per frame.

How can I fix this? Any help I really appreciate.

My completed code:

import SwiftUI

struct ContentView: View {
    var body: some View {
        ScrollView(.vertical, showsIndicators: false, content: {
            GeometryReader { reader in
                topCardView
                    .offset(y: -reader.frame(in: .global).minY)
                    .frame(width: reader.size.width, height: reader.frame(in: .global).minY + 450)
            }
            .frame(height: 450)
            
            VStack(spacing: 40) {
                BottomView
            }
            .padding(.vertical, 32)
            .padding(.horizontal, 16)
            .background(
                Color(red: 232/255, green: 230/255, blue: 225/255)
                    .cornerRadius(40)
            )
            .offset(y: -25)
        })
    }
 
    @ViewBuilder
    private var topCardView: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHGrid(rows: Array(repeating: GridItem(), count: 1), spacing: 20) {
                ForEach(0..<2) { _ in
                    RoundedRectangle(cornerRadius: 12)
                        .fill(Color(red: 248/255, green: 250/255, blue: 220/255))
                        .frame(width: 300, height: 377)
                }
            }
            .padding(.horizontal, 20)
        }
    }
    
    @ViewBuilder
    private var BottomView: some View {
        ForEach(0..<2) { _ in
            VStack(alignment: .center) {
                Text("Fastest Near You")
                    .bold()
                    .frame(width: UIScreen.main.bounds.width - 32 * 6, height: 35)
                
                Text("Order your food here")
                    .frame(width: UIScreen.main.bounds.width - 32 * 4, height: 25)
                
                LazyVGrid(columns: Array(repeating: GridItem(), count: 2), spacing: 12) {
                    ForEach(0..<4) { _ in
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color(red: 248/255, green: 250/255, blue: 220/255))
                            .frame(width: UIScreen.main.bounds.width / 2 - 24, height: 200)
                        
                    }
                }
                .padding(.top, 20)
            }
        }
    }
}

Error Image:

Error Image


Solution

  • I was asking in the comments, why you were setting a dynamic frame height at all. You replied to say, you want to create a parallax effect.

    I think this can be achieved without using a dynamic frame height:

    • the top section can simply use the full height of the container that surrounds it (the GeometryReader)
    • for the parallax effect, a y-offset can be applied to the top section that is a fraction of the scrolled height
    • if you are running iOS 17 or above, you can use .frame(in: .scrollView) to reference the coordinate space of the ScrollView
    • for earlier iOS versions, you could name the coordinate space of the ScrollView instead
    • to manage the initial spacing between the two sections, use a VStack that contains them both
    • I would suggest applying a zIndex to the bottom view, just to be quite sure that it is always shown above the top view
    • an outer GeometryReader can be used to measure the size of the screen, instead of using UIScreen.main (which is deprecated).

    So maybe try something like this:

    struct ContentView: View {
        var body: some View {
            GeometryReader { outer in
                let screenSize = outer.size
                ScrollView(.vertical, showsIndicators: false) {
                    VStack(spacing: 40) {
                        GeometryReader { reader in
                            let minY = reader.frame(in: .scrollView).minY
                            topCardView
                                .frame(maxHeight: .infinity)
                                .offset(y: -minY / 2)
                        }
                        .frame(height: screenSize.height / 2)
    
                        VStack(spacing: 40) {
                            bottomView(screenSize: screenSize)
                        }
                        .padding(.horizontal, 16)
                        .padding(.bottom, 40)
                        .background(
                            RoundedRectangle(cornerRadius: 40)
                                .fill(Color(red: 232/255, green: 230/255, blue: 225/255))
                        )
                        .zIndex(1)
                    }
                }
            }
        }
    
        @ViewBuilder
        private var topCardView: some View {
            // content as before
        }
    
        @ViewBuilder
        private func bottomView(screenSize: CGSize) -> some View {
            // content as before, but using screenSize instead of UIScreen.main.bounds
        }
    }
    

    Animation