最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

lazyvstack - Double sticky header in SwiftUI - Stack Overflow

programmeradmin1浏览0评论

Here's a full code example of an attempt at a double sticky header view:

struct ContentView: View {
    var body: some View {
        ScrollView(showsIndicators: false) {
            LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
                Section {
                    SecondView()
                } header: {
                    HeaderView()
                }
            }
        }
    }
}

struct HeaderView: View {
    var body: some View {
        Rectangle()
            .fill(.green)
            .frame(height: 50)
    }
}

struct SecondView: View {
    var body: some View {
        LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                VStack {
                    ForEach(0..<20) { index in
                        ItemView(index: index)
                    }
                }
            } header: {
                SecondHeaderView()
            }
        }
    }
}

struct SecondHeaderView: View {
    var body: some View {
        Rectangle()
            .fill(.red)
            .frame(height: 60)
    }
}

struct ItemView: View {
    let index: Int
    var body: some View {
        VStack {
            Text("Item \(index)")
                .padding()
        }
        .frame(height: 80)
        .background(.gray)
    }
}

#Preview {
    ContentView()
}

Note that initially, the red SecondHeaderView is correctly positioned below the green HeaderView. However, when scrolling, the red header scrolls beneath the green header until it sticks at the same y position as the green header.

I need the red header to stay anchored below the green header. Note that SecondView may not always be embedded in a ContentView. It could live by itself and still need its own single sticky header to work, so I think both views need the LazyVStack.

I've messed with various GeometryReader view offsets, trying to get the red header to position correctly but I can't get it right.

Here's a full code example of an attempt at a double sticky header view:

struct ContentView: View {
    var body: some View {
        ScrollView(showsIndicators: false) {
            LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
                Section {
                    SecondView()
                } header: {
                    HeaderView()
                }
            }
        }
    }
}

struct HeaderView: View {
    var body: some View {
        Rectangle()
            .fill(.green)
            .frame(height: 50)
    }
}

struct SecondView: View {
    var body: some View {
        LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                VStack {
                    ForEach(0..<20) { index in
                        ItemView(index: index)
                    }
                }
            } header: {
                SecondHeaderView()
            }
        }
    }
}

struct SecondHeaderView: View {
    var body: some View {
        Rectangle()
            .fill(.red)
            .frame(height: 60)
    }
}

struct ItemView: View {
    let index: Int
    var body: some View {
        VStack {
            Text("Item \(index)")
                .padding()
        }
        .frame(height: 80)
        .background(.gray)
    }
}

#Preview {
    ContentView()
}

Note that initially, the red SecondHeaderView is correctly positioned below the green HeaderView. However, when scrolling, the red header scrolls beneath the green header until it sticks at the same y position as the green header.

I need the red header to stay anchored below the green header. Note that SecondView may not always be embedded in a ContentView. It could live by itself and still need its own single sticky header to work, so I think both views need the LazyVStack.

I've messed with various GeometryReader view offsets, trying to get the red header to position correctly but I can't get it right.

Share Improve this question asked Feb 7 at 18:04 soleilsoleil 13.1k33 gold badges117 silver badges194 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

Here are two possible ways to solve

1. Combine the headers together

An easy way to solve is to combine the two headers together. If it is possible that SecondView may be shown in isolation then you can pass a flag, to indicate whether the header should be shown or not:

// ContentView

ScrollView(showsIndicators: false) {
    LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
        Section {
            SecondView()
        } header: {
            VStack(spacing: 0) {
                HeaderView()
                SecondHeaderView()
            }
        }
    }
}
struct SecondView: View {
    var showHeader = false
    var body: some View {
        LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                // ...
            } header: {
                if showHeader {
                    SecondHeaderView()
                }
            }
        }
    }
}

To show SecondView in isolation:

// ContentView

ScrollView(showsIndicators: false) {
    SecondView(showHeader: true)
}

2. Apply padding to the second header

Alternatively, if it can be assumed that the second header will be in the correct position when initially shown then this position can be measured in .onAppear. Then, to keep it in the same position, top padding can be applied to compensate for any scroll movement.

The same padding must be applied as negative top padding to the scrolled content, otherwise the content doesn't move until the drag movement reaches the height of the first header.

I found that the header was making tiny movements when scrolling was happening, which was causing errors in the console about "action tried to update multiple times per frame". These errors can be prevented by checking that the adjustment differs from the previous amount by a threshold amount, 0.1 works fine.

struct SecondView: View {
    @State private var initialOffset = CGFloat.zero
    @State private var topPadding = CGFloat.zero

    var body: some View {
        LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                VStack {
                    // ...
                }
                .padding(.top, -topPadding)
            } header: {
                SecondHeaderView()
                    .padding(.top, topPadding)
                    .background {
                        GeometryReader { proxy in
                            let minY = proxy.frame(in: .scrollView).minY
                            Color.clear
                                .onAppear {
                                    initialOffset = minY
                                }
                                .onChange(of: minY) { oldVal, newVal in
                                    let adjustment = max(0, initialOffset - newVal)
                                    if abs(topPadding - adjustment) > 0.1 {
                                        topPadding = adjustment
                                    }
                                }
                        }
                    }
            }
        }
    }
}

With this approach, SecondView can be shown in isolation without needing to pass a flag.


If it can't be assumed that the second header will be in the correct position at initial show then the height of the parent header can be passed as a parameter instead. This is perhaps a safer way of solving when using the padding approach:

struct ContentView: View {
    @State private var headerHeight = CGFloat.zero

    var body: some View {
        ScrollView(showsIndicators: false) {
            LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
                Section {
                    SecondView(parentHeaderHeight: headerHeight)
                } header: {
                    HeaderView()
                        .onGeometryChange(for: CGFloat.self) { proxy in
                            proxy.size.height
                        } action: { height in
                            headerHeight = height
                        }
                }
            }
        }
    }
}

struct SecondView: View {
    var parentHeaderHeight = CGFloat.zero
    @State private var topPadding = CGFloat.zero

    var body: some View {
        LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
            Section {
                VStack {
                    // ...
                }
                .padding(.top, -topPadding)
            } header: {
                SecondHeaderView()
                    .padding(.top, topPadding)
                    .onGeometryChange(for: CGFloat.self) { proxy in
                        proxy.frame(in: .scrollView).minY
                    } action: { minY in
                        let adjustment = max(0, parentHeaderHeight - minY)
                        if abs(topPadding - adjustment) > 0.1 {
                            topPadding = adjustment
                        }
                    }
            }
        }
    }
}

Both approaches work the same. This is how it looks for the case of the double header:

发布评论

评论列表(0)

  1. 暂无评论