SwiftUI Scrollview with both axes enabled produces layout mess

The Problem

In our brand new SwiftUI project (Multiplatform app for iPhone, iPad and Mac) we are using a ScrollView with both .horizontal and .vertical axes enabled. In the end it all looks like a spreadsheet.

Inside of ScrollView we are using LazyVStack with pinnedViews: [.sectionHeaders, .sectionFooters]. All Footer, Header and the content cells are wrapped into a LazyHStack each. The lazy stacks are needed due to performance reasones when rows and columns are growing.

And this exactly seems to be the problem. ScrollView produces a real layout mess when scrolling in both axes at the same time.

Tested Remedies

I tried several workarounds with no success.
We built our own lazy loading horizontal stack view which shows only the cells whose indexes are in the visible frame of the scrollview, and which sets dynamic calculated leading and trailing padding.

I tried the Introspect for SwiftUI packacke to set usesPredominantAxisScrolling for macOS and isDirectionalLockEnabled for iOS. None of this works as expected.

I also tried a workaround to manually lock one axis when the other one is scrolling. This works fine on iOS but it doesn't on macOS due to extreme poor performance (I think it's caused by the missing NSScrollViewDelegate and scroll events are being propagated via NotificationCenter).

Example Screen Recordings

To give you a better idea of what I mean, I have screenshots for both iOS and macOS.

Example Code

And this is the sample code used for the screenshots. So you can just create a new Multiplatform project and paste the code below in the ContentView.swift file. That's all.

import SwiftUI

let numberOfColums: Int = 150
let numberOfRows: Int = 350

struct ContentView: View {
    var items: [[Item]] = {
        var items: [[Item]] = [[]]
        for rowIndex in 0..<numberOfRows {
            var row: [Item] = []
            for columnIndex in 0..<numberOfColums {
                row.append(Item(column: columnIndex, row: rowIndex))
            }
            items.append(row)
        }
        return items
    }()

    var body: some View {
        Group {
            ScrollView([.horizontal, .vertical]) {
                LazyVStack(alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
                    Section(header: Header()) {
                        ForEach(0..<items.count, id: \.self) { rowIndex in
                            let row = items[rowIndex]
                            LazyHStack(spacing: 1) {
                                ForEach(0..<row.count, id: \.self) { columnIndex in
                                    Text("\(columnIndex):\(rowIndex)")
                                        .frame(width: 100)
                                        .padding()
                                        .background(Color.gray.opacity(0.2))
                                }
                            }
                        }
                    }
                }
            }
            .edgesIgnoringSafeArea([.top])
        }
        .padding(.top, 1)
    }
}

struct Header: View {
    var body: some View {
        LazyHStack(spacing: 1) {
            ForEach(0..<numberOfColums, id: \.self) { idx in
                Text("Col \(idx)")
                    .frame(width: 100)
                    .padding()
                    .background(Color.gray)
            }
            Spacer()
        }
    }
}

struct Item: Hashable {
    let id: String = UUID().uuidString
    var column: Int
    var row: Int
}

Can You Help?

Okay, long story short...
Does anybody knows a real working solution for this issue?
Is it a known SwiftUI ScrollView bug?

If there were a way to lock one scrolling direction while the other is scrolled, then I could deal with that. But at the moment, unfortunately, it is not usable for us.

So any solution is highly appreciated! Ideally an Apple engineer can help. 😃


NOTE: Unfortunately Apple does not allow URLs to anywhere. I also have two screen recordings for both iOS and macOS. So, if you would like to watch them, please contact me.

I don't think you need to add the axis. I remember it' automatic.

We are also facing the same issue, Our app layout looks similar to this. did you find any other way of implementing this kind of layout?

This might work:

        ScrollView(.horizontal) {
            ScrollView(.vertical) {
                LazyVStack(alignment: .leading, spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
                    Section(header: Header()) {

I got the idea from here: https://developer.apple.com/forums/thread/685941

Try this one maybe it will be helpful to you. https://github.com/fauadanwar/SwiftUITutorial/tree/main/SwiftUISpreadSheet.

Note: About the issue I raised a ticket with Apple the got my ticket back with the reply "It's an OS issue and may get fixed in upcoming OS versions." Also noticed that Apple docs says "Lazy stacks trade some degree of layout correctness for performance"

In case someone else is looking for a solution (for MacOs), I discovered that although you can call ScrollView(.horizontal) or ScrollView(.vertical), writing ScrollView(.horizontal, .vertical) does not work. For some reason you have to add brackets: ScrollView([.horizontal, .vertical]).

SwiftUI Scrollview with both axes enabled produces layout mess
 
 
Q