iosswiftuiscrollviewswiftui-listlazyvgrid

(SwiftUI) Segmented Picker in a pinned SectionHeader does not Update ScrollView ContentHeight as expected


Within a Scrollview, i am trying to have a pinned SectionHeader with a simple segmented PickerView, that can switch to two different item Stacks. One very long and one very short. This works fine, as long as the SectionHeader is not sticky. As soon as it does become sticky, the problems start:

As you can see in the video, if i scroll the long left side (while sticky) and then switch to the short right side, the stickyHeader loses its anchored position. Is there any way to prevent this from happening?I tried several things already without any success. (For example, a GeometryReaders proxy that scrolls manually to the top as soon as i switch the tap)

From my Understanding, the problem lies within the ScrollViews ContentHeight, which doesn't get updated correctly. This is very much visible, as the ScrollViewIndicator does not get a visual update in his length also.

Is this possible to achieve, or is the PickerView not made to work within a List of multiple Sections at all? Is there any way to update the ScrollView ContentHeight in a way, that the stickyHeader keeps its position?

Any Hint is much appreciated!

I've also added a video and the source code for reference.

Issue with Sticky Picker and ScrollView Update When Switching Lists

struct ContentView: View {
    @State private var tab = 0

    var body: some View {
        VStack(spacing: 0) {
            let cols = [GridItem(.flexible())]
            
            ScrollView {
                LazyVGrid(columns: cols, pinnedViews: [.sectionHeaders]) {
                    
                    Section {
                        ForEach(1...2, id: \.self) { count in
                            Text("Section 1 Item \(count)")
                                .frame(maxWidth: .infinity, alignment: .leading)
                              .padding().border(Color.blue)
                        }
                    } header: {
                        Text("Section 1")
                            .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
                            .background(Color.blue)
                    }

                    Section {
                        if tab == 0 {
                            ForEach(10...20, id: \.self) { count in
                                Text("Section 2 Tab 0 Item \(count)")
                                  .frame(maxWidth: .infinity, alignment: .leading)
                                  .padding().border(Color.purple)
                            }
                        }
                        
                        if tab == 1 {
                            ForEach(3...5, id: \.self) { count in
                                Text("Section 2 Tab 1 Item \(count)")
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                  .padding().border(Color.purple)
                            }
                        }

                    } header: {
                        Picker("", selection: $tab) {
                            Text("Long").tag(0)
                            Text("Short").tag(1)
                        }
                        .pickerStyle(.segmented).padding().background(Color.purple)
                    }
                }

                LazyVGrid(columns: cols) {
                        Section {
                            ForEach(30...50, id: \.self) { count in
                                Text("Section 3 Item \(count)")
                                    .frame(maxWidth: .infinity, alignment: .leading)
                                    .padding().border(Color.green)
                            }
                        } header: {
                            Text("Section 3")
                                .frame(maxWidth: .infinity, minHeight: 50, alignment: .center)
                                .background(Color.green)
                            
                        }
                }
            }
        }
        .font(.caption)
    }
}

#Preview {
    ContentView()
}

Solution

  • You could try setting the scroll position whenever the tab is switched.

    One way to do this is to use .scrollPosition on the ScrollView, together with .scrollTargetLayout on the containers inside the ScrollView. Then add an .onChange callback to detect a change of tab and update the scroll position when a change happens. This brings the header with the picker back again, if it was off-screen after the tab change.

    @State private var scrollPosition: Int?
    
    ScrollView {
        LazyVGrid(columns: cols, pinnedViews: [.sectionHeaders]) {
            // ...
        }
        .scrollTargetLayout()
    
        LazyVGrid(columns: cols) {
            // ...
        }
        .scrollTargetLayout()
    }
    .scrollPosition(id: $scrollPosition)
    .onChange(of: tab) { oldVal, newVal in
        withAnimation {
            scrollPosition = newVal == 0 ? 10 : 3
        }
    }
    

    Notes:

    Animation