There is a jumping content layout issue when a LazyHStack
is embedded in a ScrollView
. When the child component is a Button
embedded in a VStack
a weird view "jumping" behavior occurs when displayed. This happens both on first and redisplay of the child component. The following code will reproduce the problem in iOS 18
on an iPhone 16 Pro Simulator
. This isn't reproducible in a Xcode preview. Scrolling slowly makes the issue happen more often.
import SwiftUI
struct ContentView: View {
let elements = (0..<50).map(\.description)
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(elements, id: \.self) { element in
VStack {
Button(action: {}) {
Text("\(element)")
.foregroundStyle(Color.white)
.frame(width: 120, height: 80)
.background(Color.blue)
.cornerRadius(12)
}
}
}.padding()
}
}
}
}
This issue can be patched by removing the VStack
, removing the Button
, using a HStack instead of a LazyHStack, or adding a .id(element)
to the Button
. These fixes aren't desired and I would love a stronger understanding of what is going on here.
The obvious fix is to remove the VStack
but I would like to add additional content to the VStack. The demo code above is to just show that the VStack doesn't need other child elements for this problem to be reproducible. Also we need to use a LazyHStack because the elements list can get quite long. Also would prefer to stay in SwiftUI land and not implement via UICollectionView.
NOTE: Using a LazyHGrid with a single row doesn't fix this issue.
Adding .geometryGroup()
to the VStack
seems to fix it:
ScrollView(.horizontal) {
LazyHStack {
ForEach(elements, id: \.self) { element in
VStack {
Button(action: {}) {
// as before
}
}
.geometryGroup() // 👈 HERE
}
.padding()
}
}