I was expecting the scroll view to honour the safe area. Why wouldn't it here? And what is scrollClipDisabled
?
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!")
}
}
}
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)
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)
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)
}
}