1

I want to make headers sticky to top when scrolling vertically. enter image description here

The code is something like this:

ScrollView([.vertical]) {
    LazyVStack(alignment: .leading, pinnedViews: .sectionHeaders) {
        ForEach(...) {
            ScrollView([.horizontal]) {
                LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
                    Section {
                       ...
                    } header: {
                       ... // <-- the header
                    }
                }
            }
        }
    }
}

However, it seems that in nested scroll views, the pinnedViews property can only pin views in the nearest scroll view.

Chocoford
  • 183
  • 8

1 Answers1

2

Finally figure out the problem.

enter image description here

Including some answers such as https://stackoverflow.com/a/71154712/9073579, I ended up using a single ScrollView to achieve this functionality.

This approach also introducing the problem that it caused the view to be centered when the height of the inner content was not enough.

Thus, I had to spend additional effort to address this issue. This required additional variables to maintain some state, such as the width of the entire scroll area, and the calculated width of the elements that need to be sticky.

@State private var width: CGFloat? = nil

var body: some View {
    ScrollViewReader { proxy in
        GeometryReader { geometry in // <-- to get the viewport size
            ScrollView([.vertical, .horizontal]) {
                ZStack(alignment: .topLeading) { // to place the scroll anchor
                    VStack(alignment: .leading) {
                        VStack(alignment: .leading) {
                            // my headers view here
                        }
                        .stickyHorizontal(geometry.size.width, scrollWidth: width)

                        ForEach(0..<2) { i in
                            LazyVStack(alignment: .leading, spacing: 0, pinnedViews: .sectionHeaders) {
                                Section {
                                    ... // body table view
                                } header: {
                                    ... // table header
                                    // calculating the width of header (i.e. the width of table)
                                    .background(GeometryReader { geometry in
                                        Color.clear
                                            .onAppear {
                                                width = geometry.size.width
                                            }
                                            .onChange(of: geometry.size.width) { newValue in
                                                width = newValue
                                            }
                                    })
                                }
                            }
                        }
                      
                        // addtional views (footer)
                                                ...
                      
                        // act as a Spacer(). Stretching the VStack.
                        Color.clear
                            .frame(height: geometry.size.height - 40)
                    }
                    .frame(width: width)

                    // scroll destination
                    Color.clear
                        .frame(width: 1, height: 1)
                        .id("topLeading")
                }
                .onAppear {
                    // make sure the scroll view appears from top leading.
                    proxy.scrollTo("topLeading", anchor: .topLeading)
                }
            }
        }
    }
}

extension View {
    @ViewBuilder
    func stickyHorizontal(_ viewPortWidth: CGFloat, scrollWidth: CGFloat?) -> some View {
        LazyHStack( spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                Color.clear
                    .frame(width: scrollWidth == nil ? nil : scrollWidth! - viewPortWidth)
            } header: {
                self
                    .frame(width: viewPortWidth)
            }
        }
    }
}

Chocoford
  • 183
  • 8