Estou tentando criar uma ScrollView com um LazyVStack onde dois cabeçalhos são fixados no topo. Também deve funcionar dentro de um NavigationStack usando o modo de exibição grande para o título. Ambos os cabeçalhos devem estar dentro da ScrollView para que a animação do título grande funcione durante a rolagem (passando do título grande para o pequeno ao rolar para baixo).
Foi isso que eu tentei:
struct StickyHeaderHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
let scrollCoordinateSpace = "scroll"
@State private var headerHeight: CGFloat = 0
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
GeometryReader { geometry in
let frame = geometry.frame(in: .named(scrollCoordinateSpace))
let offset = max(0, -frame.minY)
HStack {
Image(systemName: "pin.fill")
Text("Sticky Header")
.font(.headline)
Spacer()
}
.padding()
.background(Color.red.opacity(0.8))
.offset(y: offset)
.background(
GeometryReader { proxy in
Color.clear
.preference(key: StickyHeaderHeightKey.self, value: proxy.size.height)
}
)
}
.frame(height: headerHeight)
.zIndex(1)
// --- Main Content ---
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
ForEach(0..<50) { index in
Section {
VStack(alignment: .leading, spacing: 0) {
Text("Content \(index + 1)")
.padding(.vertical, 10)
.padding(.horizontal)
Text("Content \(index + 1)")
.padding(.vertical, 10)
.padding(.horizontal)
Text("Content \(index + 1)")
.padding(.vertical, 10)
.padding(.horizontal)
}
.background(Color.green.opacity(0.5))
} header: {
VStack(spacing: 0) {
Text("Header")
.padding(.vertical, 6)
.padding(.horizontal)
.background(Color.blue.opacity(0.5))
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.blue.opacity(0.5))
}
}
}
}
}
.coordinateSpace(name: scrollCoordinateSpace)
.navigationTitle("Large Title")
.navigationBarTitleDisplayMode(.large)
.onPreferenceChange(StickyHeaderHeightKey.self) { value in
headerHeight = value
}
}
}
}
O problema que estou enfrentando é que os cabeçalhos da seção LazyVStack são sempre fixados na posição mais alta do ScrollView e não abaixo do cabeçalho vermelho fixo.
Uma maneira de resolver pode ser mostrar o cabeçalho superior (vermelho) como
safeAreaInset
umScrollView
.Boa parte do conteúdo que você tinha antes pode ser descartado. Em particular, nenhum dos seguintes itens é necessário:
PreferenceKey
VStack
GeometryReader
que estava medindo a altura do cabeçalhoAqui está o exemplo (completo) atualizado para mostrar que está funcionando:
EDITAR Em um comentário você disse:
Imagino que você esteja se referindo a quando o conteúdo rolado é puxado para baixo, mais baixo do que a capacidade de rolagem. Ao ser solto, ele retorna à posição inicial. Enquanto isso acontece, o cabeçalho permanece parado. No entanto, o título de navegação se move com o conteúdo, então, quando o conteúdo é puxado para baixo, o título de navegação sobrepõe-se ao cabeçalho. O mesmo pode acontecer com uma rolagem intercalar, como pode ser visto no gif acima.
Uma maneira de manter o cabeçalho com o conteúdo quando ele é puxado para baixo é medir o deslocamento de rolagem e aplicá-lo como um deslocamento y ao cabeçalho, se for negativo.
Como seu alvo é o iOS 18, você pode usar
.onScrollGeometryChange
para medir o deslocamento rolado e salvar isso em uma variável de estado:EDIT2 Em vez de usar
.onScrollGeometryChange
para medir o deslocamento da rolagem, você também pode usar.onGeometryChange
para medir a posição doLazyVStack
no espaço de coordenadas doScrollView
. Funciona da mesma forma: