iosswiftswiftuiswiftui-scrollview

Default space in SwiftUI ScrollView


I am trying to create a custom scroll view which can notify the parent view if the user has scrolled to the end.

This is how I implement this:

struct CustomVerticalScrollView<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        ScrollView {
            content
            
            Color
                .clear
                .frame(width: 0, height: 0)
                .modifier(ViewAppearsOnScreen())
                .onPreferenceChange(IsOnScreenKey.self) { value in
                    if value == true {
                        print("Has reached end!")
                    }
                }
        }
        .background(.green)
    }
}

struct ViewAppearsOnScreen: ViewModifier {
    func body(content: Content) -> some View {
        GeometryReader { geometry in
            // Check if this view's frame intersects with the visible screen bounds
            content
                .preference(
                    key: IsOnScreenKey.self,
                    value: isViewVisible(geometry: geometry)
                )
        }
    }
    
    // Determines if the view is visible within the screen's bounds
    private func isViewVisible(geometry: GeometryProxy) -> Bool {
        let frame = geometry.frame(in: .global) // Get the frame in global space
        let screenBounds = UIScreen.main.bounds // Screen boundaries
        
        return screenBounds.intersects(frame) // Check if it intersects with screen bounds
    }
}

The code used here to detect the end of the scroll view was taken from this answer

And then this is how I use it:

struct ContentView: View {
    var body: some View {
        
        CustomVerticalScrollView{
            VStack(spacing: .zero) {
                Color
                    .blue
                    .frame(maxWidth: .infinity)
                    .frame(height: 1000)
            }
        }
    }
}

The functionality works well, however, This gives me the following results with some extra spacing:

Custom scrollview swiftUI end of scroll reached

  1. When I don't have clear color view, it doesn't seem to add any spacing at the end, even though there is no padding, spacing and the view's height is 0 (third item in the table)
  2. When I add the clear color view, it adds some considerable spacing at the end (first item in the table)
  3. If I remove the two mentioned modifiers from the second item in the table, there is still some padding - probably the Geometry reader is at play here

I though setting the height to 0 and using the Geometry reader would not impact the spacing.

Just wondering how can I eliminate all this extra spacing so that I can have this additional view in there.


Solution

  • GeometryReader in this case will resize itself to its ideal height, which happens to be a small, non-zero value. You can move frame(width: 0, height: 0) to after onPreferenceChange to fix this.

    Every SwiftUI view has its own "preferred spacing" on all four edges, and this spacing can differ depending on the type of the other view. For example, the preferred spacing between a Text and an Image is a bit larger than the preferred spacing between two Texts. The default layout behaviour respects this spacing preference. When implementing your own Layouts, you can read this preferred spacing using LayoutSubview.spacing.

    Using a VStack with an explicitly spacing: parameter makes the VStack layout not respect the spacing preference.

    ScrollView {
        VStack(spacing: 0) {
            content
            Color
                .clear
                .modifier(ViewAppearsOnScreen())
                .onPreferenceChange(IsOnScreenKey.self) { ... }
                .frame(width: 0, height: 0)
        }
    }