swiftuilayout

Scroll view naturally going beyond safe area?


I was expecting the scroll view to honour the safe area. Why wouldn't it here? And what is scrollClipDisabled ?

enter image description here

struct ContentView: View {
    
    var body: some View {
        ZStack {
            Color.red
            PrimaryView()
                .scrollClipDisabled(false)
        }
    }
}

struct PrimaryView: View {
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0...100, id: \.self) { _ in
                    hello
                }
            }
            .padding()
        }
    }
    
    private var hello: some View {
        HStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
    }
}

Solution

  • I agree that it is strange that the ScrollView seems determined to ignore the safe area edges, even when you set .scrollClipDisabled(false) as you are doing. I find the double-negative labelling a bit confusing too: (clip disabled == false) means (clip enabled == true).

    If you add a border to the ScrollView, you will see that the frame is observing the safe area edges, but the scrollable content is not:

    ScrollView(.horizontal) {
        // ...
    }
    .border(.yellow, width: 2)
    

    Screenshot

    One workaround is to add a .clipShape to the ScrollView:

    PrimaryView()
        .scrollClipDisabled(false)
        .clipShape(Rectangle())
    

    However, using this approach, the scroll indicator disappears off to the sides.


    If the scroll indicator needs to be visible, as it is in your example, then another workaround is to add nominal horizontal padding to break the contact with the safe area. The size of the padding only needs to be 1 pixel. The environment value pixelLength gives this size in points:

    @Environment(\.pixelLength) private var pixelLength
    
    PrimaryView()
        .scrollClipDisabled(false)
        .padding(.horizontal, pixelLength)
    

    Screenshot


    To make the solution made more general-purpose, the safe area insets can be measured by wrapping the ScrollView with a GeometryReader. Then, padding only needs to be added if the insets are non-zero:

    GeometryReader { proxy in
        let horizontalInsets = proxy.safeAreaInsets.leading + proxy.safeAreaInsets.trailing
        ZStack {
            Color.red
            PrimaryView()
                .scrollClipDisabled(false)
                .padding(.horizontal, horizontalInsets > 0 ? pixelLength : 0)
        }
    }