SwiftUI:ScrollView中第二个LazyVStack可见时如何隐藏BottomRoundedRectangle?
问题:SwiftUI中滚动到第二个LazyVStack时无法隐藏底部视图
我尝试在第二个LazyVStack显示在屏幕上时隐藏BottomRoundedRectangle,但功能失效,且.onPreferenceChange(OffsetKey.self)中的value值始终不变。相关代码如下:
struct OffsetKey: PreferenceKey { typealias Value = CGFloat static let defaultValue: Value = .zero static func reduce(value: inout Value, nextValue: () -> Value) { value = nextValue() } } struct OffsetKeyBackground: ViewModifier { func body(content: Content) -> some View { GeometryReader { reader in content .preference( key: OffsetKey.self, value: -reader.frame(in: .named("secondLazyStack")).origin.y ) } } } struct ScrollQuestion: View { @State private var elementValue: CGFloat = .zero var body: some View { ScrollView(.vertical) { // Dynamic long text there LazyVStack { RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(0.3)) .frame(height: 920) // dynamic text, 920 for example, may be bigger } // 2nd LazyVStack LazyVStack { RoundedRectangle(cornerRadius: 12) .frame(height: 820) } .coordinateSpace(name: "secondLazyStack") } .safeAreaInset(edge: .bottom) { // How to hide this correctly when we see second LazyVStack ? BottomRoundedRectangle() // .opacity(elementValue < 50 ? 1 : 0) } .modifier(OffsetKeyBackground()) .onPreferenceChange(OffsetKey.self) { value in debugPrint(value) elementValue = value } } } struct BottomRoundedRectangle: View { var body: some View { ZStack { RoundedRectangle(cornerRadius: 8) .frame(height: 40) .foregroundStyle(.indigo) .padding(.horizontal) Text("Visible only in first LazyVStack") .foregroundStyle(.white) } } } #Preview { ScrollQuestion() }
问题根源
- 偏好设置位置错误:把
OffsetKeyBackground修饰在ScrollView外层,导致GeometryReader只能获取整个ScrollView的坐标,无法跟踪第二个LazyVStack的滚动位置。 - 坐标空间引用错误:使用第二个LazyVStack自身的坐标空间(
.named("secondLazyStack")),其原点在自身左上角,无法正确计算它在屏幕中的偏移量。
修复方案
步骤1:调整偏好设置的挂载位置
将OffsetKeyBackground从ScrollView移到第二个LazyVStack上,让GeometryReader专门跟踪这个视图的位置。
步骤2:修正坐标空间计算
改用.global坐标空间获取第二个LazyVStack的顶部Y值,这样能准确判断它是否进入屏幕视野。
步骤3:调整隐藏逻辑
根据第二个LazyVStack的顶部Y值与屏幕高度的对比,决定是否隐藏底部视图。
修改后的完整代码
struct OffsetKey: PreferenceKey { typealias Value = CGFloat static let defaultValue: Value = .zero static func reduce(value: inout Value, nextValue: () -> Value) { value = nextValue() } } struct OffsetKeyBackground: ViewModifier { func body(content: Content) -> some View { content .background( GeometryReader { reader in Color.clear .preference( key: OffsetKey.self, value: reader.frame(in: .global).minY ) } ) } } struct ScrollQuestion: View { @State private var secondStackTopY: CGFloat = .zero private let screenHeight = UIScreen.main.bounds.height var body: some View { ScrollView(.vertical) { // 动态长文本区域 LazyVStack { RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(0.3)) .frame(height: 920) // 示例高度,实际为动态内容 } // 第二个LazyVStack LazyVStack { RoundedRectangle(cornerRadius: 12) .frame(height: 820) } .modifier(OffsetKeyBackground()) } .safeAreaInset(edge: .bottom) { BottomRoundedRectangle() // 当第二个LazyVStack顶部进入屏幕时隐藏 .opacity(secondStackTopY > screenHeight ? 1 : 0) } .onPreferenceChange(OffsetKey.self) { value in debugPrint("第二个LazyVStack顶部Y值: \(value)") secondStackTopY = value } } } struct BottomRoundedRectangle: View { var body: some View { ZStack { RoundedRectangle(cornerRadius: 8) .frame(height: 40) .foregroundStyle(.indigo) .padding(.horizontal) Text("仅在第一个LazyVStack中可见") .foregroundStyle(.white) } } } #Preview { ScrollQuestion() }
关键说明
- 把偏好设置放在第二个LazyVStack的
background里,确保GeometryReader实时跟踪该视图的全局位置。 - 使用
.global坐标空间获取的minY值代表视图顶部在屏幕中的位置,当该值小于屏幕高度时,说明视图已进入视野。 - 用
screenHeight作为判断阈值,适配不同设备的屏幕尺寸。
内容的提问来源于stack exchange,提问作者Nick Rossik
相关产品推荐
相关产品推荐

