I'm encountering a layout issue in SwiftUI on iOS 17 where Color
views within a VStack
inside a ScrollView
begin to overlap as I scroll down. Each Color
view is supposed to take up the full container's relative frame, but as I scroll, the next Color
view encroaches upon the space of the previous one. This issue compounds with further scrolling, with subsequent views taking more and more space.
I have attached a video to demonstrate the issue, and here is the code to reproduce the problem. Just copy and paste it.
struct TestView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
ForEach(0..<10, id:\.self) { idx in
Color.random()
.containerRelativeFrame([.horizontal, .vertical])
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
}
#Preview {
TestView()
}
I'm using .containerRelativeFrame([.horizontal, .vertical])
for each color view to make them fill the available space, and .scrollTargetLayout()
on the VStack
. The .scrollTargetBehavior(.paging)
modifier is applied to the ScrollView
to enable paging behavior.
The expected result is for each color view to occupy its own page space without affecting the others. However, when I scroll, the layout starts to break as shown in the video.
Has anyone experienced something similar or can provide insight into what might be causing this issue?
My Environment:
Simulator running version: iOS 17.0
XCode Version: 15.0.1 (15A507)
Swift Version: swift-driver version: 1.87.1 Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1) Target: x86_64-apple-macosx13.0
I think this must be a bug.
It seems to be compensating for the bottom insets incorrectly. If you try it on an iPhone SE then it works, because the bottom insets are 0. Otherwise, the "slippage" appears to be half the size of the bottom insets.
Possible workarounds:
ScrollView(.vertical, showsIndicators: false) {
// content as before
}
.scrollTargetBehavior(.paging)
.ignoresSafeArea(edges: .bottom) // <- ADDED
GeometryReader
to measure the safe area insets and set the VStack
spacing accordinglyGeometryReader { proxy in // <- ADDED
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: proxy.safeAreaInsets.bottom / 2) { // <- CHANGED
// content as before
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
With this approach, you can eliminate the space between the colors that is seen with solution 2.
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
ForEach(0..<10, id:\.self) { idx in
Color.clear
.containerRelativeFrame([.horizontal, .vertical])
.padding(.bottom, proxy.safeAreaInsets.bottom / 2)
.background(Color.random())
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
.containerRelativeFrame
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
ForEach(0..<10, id:\.self) { idx in
Color.random()
.frame(
width: proxy.size.width,
height: proxy.size.height + (proxy.safeAreaInsets.bottom / 2)
)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
}
I would go for the first workaround if you can, because if the bug is fixed in a future version then it should still continue to work.
Ps. You might want to report the bug as feedback to Apple.