buttonswiftuiscrollviewios18lazyhstack

SwiftUI content jumping issue with LazyHStack embedded in a horizontal ScrollView in iOS 18


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()
            }
        }
    }
}

Preview

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.


Solution

  • 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()
        }
    }