Я нашел приведенную ниже версию, но не слишком ей доверяю, и она не очень гибкая.
// MARK: - B over A with dynamic offset; A interactive while visible
struct CollapsingOverlayScrollExample: View {
private let headerHeight: CGFloat = 200
@State private var scrolledUp: CGFloat = 0
@State private var baselineTop: CGFloat? = nil
@State private var displayedScrolledUp: CGFloat = 0
var body: some View {
ZStack(alignment: .top) {
// View A (Header) behind
VStack {
Text("
.font(.headline)
.foregroundColor(.white)
Text("Visible part remains interactive")
.foregroundColor(.white)
}
.frame(height: headerHeight)
.frame(maxWidth: .infinity)
.background(Color.blue)
.onTapGesture {
print("
}
.zIndex(0)
// View B (ScrollView) on top, offset down by remaining visible header
ScrollView {
VStack(spacing: 20) {
// Sentinel to track scroll offset within the ScrollView
GeometryReader { geo in
Color.clear
.preference(key: ScrollOffsetKey.self,
value: geo.frame(in: .named("ROOT")).minY)
}
.frame(height: 0)
ForEach(1...30, id: \.self) { i in
Text("Scroll Item \(i)")
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(Color.orange.opacity(0.9))
.cornerRadius(8)
.padding(.horizontal)
}
}
.padding(.top, 16)
}
.background(Color.green.opacity(0.5))
// While header is visible, keep B's frame below it so A remains tappable
.offset(y: max(0, headerHeight - displayedScrolledUp))
.zIndex(1)
}
.coordinateSpace(name: "ROOT")
.onPreferenceChange(ScrollOffsetKey.self) { topInRoot in
// Calibrate baseline once to avoid initial jump due to layout timing.
if baselineTop == nil {
baselineTop = topInRoot
}
let base = baselineTop ?? headerHeight
// Positive distance scrolled upwards relative to the baseline.
let delta = base - topInRoot
scrolledUp = max(0, delta)
}
.onChange(of: scrolledUp) { newValue in
withAnimation(.interactiveSpring(response: 0.25, dampingFraction: 0.86, blendDuration: 0.2)) {
displayedScrolledUp = newValue
}
}
.edgesIgnoringSafeArea(.all)
}
}
Изменить: 1
Я сделал это, как показано ниже, используя ответ @Benzy, но заметил, что прыжок выглядит не слишком хорошо. Есть ли способ постепенно переместить вид прокрутки вверх (без перемещения содержимого прокрутки), и если он переместился вверх на 50 %, и пользователь поднимает палец, мы прикрепляем его к верху, а если он оставляет его ниже 50 %, мы возвращаем его обратно к представлению ниже A. Я написал фрагмент с собственным жестом, но это не вселяет во меня особой уверенности.
import SwiftUI
struct CollapsingOverlayScrollExample: View {
private let headerHeight: CGFloat = 420
private let headerCollapsedHeight: CGFloat = 60
@State private var collapsedHeaderBottom: CGFloat = 0
@State private var scrollOffset: CGFloat = 420 // Start with header collapsed
private var isScrolled: Bool {
scrollOffset > 0
}
var body: some View {
GeometryReader { geo in
ZStack(alignment: .top) {
// View A (Header)
VStack {
Text("
.font(.headline)
.foregroundStyle(.white)
Text("Visible part remains interactive")
.foregroundStyle(.white)
}
.padding(.top, geo.safeAreaInsets.top)
.frame(height: headerHeight)
.frame(maxWidth: .infinity)
.background(Color.blue)
.onTapGesture {
print("
}
// Top "nav" controls (menu on left, share on right)
.overlay(alignment: .top) {
HStack {
Button(action: { }) {
Image(systemName: "line.3.horizontal")
.font(.system(size: 20, weight: .semibold))
.foregroundColor(Color(.label))
.frame(width: 44, height: 44)
.background(Color(.systemGray5).opacity(0.95))
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.25), radius: 12, x: 0, y: 8)
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button(action: { }) {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 20, weight: .semibold))
.foregroundColor(Color(.label))
.frame(width: 44, height: 44)
.background(Color(.systemGray5).opacity(0.95))
.clipShape(Circle())
.shadow(color: Color.black.opacity(0.25), radius: 12, x: 0, y: 8)
}
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 20)
.padding(.top, geo.safeAreaInsets.top + 8)
}
// View B (ScrollView)
ScrollView {
LazyVStack(spacing: 20, pinnedViews: [.sectionHeaders]) {
// First section: rows 1-10 (before the pinned header)
ForEach(1...10, id: \.self) { i in
Text("Scroll Item \(i)")
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(Color.orange.opacity(0.9))
.cornerRadius(8)
.padding(.horizontal)
}
// Second section: rows 11-30 with PINNED header
Section {
ForEach(11...30, id: \.self) { i in
Text("Scroll Item \(i)")
.frame(maxWidth: .infinity)
.frame(height: 60)
.background(Color.orange.opacity(0.9))
.cornerRadius(8)
.padding(.horizontal)
}
} header: {
// PINNED header view - sticks to top when scrolling
ZStack {
Color.white
Text("
.font(.headline)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 12)
}
.frame(maxWidth: .infinity)
.shadow(color: Color.black.opacity(0.06), radius: 4, x: 0, y: 2)
.background(Color.white)
}
}
.padding(.top, 16 + (isScrolled ? geo.safeAreaInsets.top : 0))
// Measure the scroll offset using .onGeometryChange
// and .frame(in: .scrollView), compatible with iOS 17
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.frame(in: .scrollView).minY
} action: { minY in
scrollOffset = -minY
}
}
.onAppear {
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = scene.windows.first(where: { $0.isKeyWindow }) {
collapsedHeaderBottom = window.safeAreaInsets.top + headerCollapsedHeight
}
}
.background(.white)
.padding(.top, isScrolled ? collapsedHeaderBottom : headerHeight)
.animation(.easeInOut, value: isScrolled)
}
.ignoresSafeArea(edges: .top)
}
}
}
#Preview {
CollapsingOverlayScrollExample()
}
Подробнее здесь: https://stackoverflow.com/questions/798 ... eader-view
Мобильная версия