SwiftUI 实现固定底部的Sticky/Pinned Bottom Sheet弹层问题求解
问题根源
你当前的实现通过修改整个弹层容器的全局offset控制显隐,spring动画的弹性超调会让offset变为负数(弹层整体上移),自然会露出弹层下方的背景区域。
解决方案
实现底部固定的核心是将弹层的变换锚点固定在底部,所有动画、拖拽位移都以底部为基准点,这样无论顶部怎么弹跳,底部都会牢牢吸附在屏幕底部。
你只需要对原有代码做3处改动即可:
- 移除原有offset控制逻辑,改用锚点定位固定底部
- 调整动画的作用范围,仅响应isOpen状态变化
- 补充锚点配置
改后完整代码
import SwiftUI // 补充原有代码依赖的ChildSizeReader实现 struct ChildSizeReader: ViewModifier { @Binding var size: CGSize func body(content: Content) -> some View { content .background( GeometryReader { proxy in Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size) } ) .onPreferenceChange(SizePreferenceKey.self) { size = $0 } } } extension View { func readSize(_ size: Binding<CGSize>) -> some View { self.modifier(ChildSizeReader(size: size)) } } private struct SizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } } struct BottomSheetView<Content: View>: View { @Binding var isOpen: Bool @ViewBuilder let content: Content @State private var contentSize: CGSize = .zero var body: some View { GeometryReader { geometry in VStack(spacing: 0.0) { content .readSize($contentSize) } .frame( width: geometry.size.width, height: contentSize.height, alignment: .top ) .clipShape( Path( UIBezierPath( roundedRect: CGRect(x: 0.0, y: 0.0, width: geometry.size.width, height: contentSize.height + 200), // 增加裁切高度,避免超调时顶部圆角被裁切 byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 16.0, height: 16.0) ) .cgPath ) ) // 核心改动:固定底部锚点 .position(x: geometry.size.width / 2, y: geometry.size.height) .anchorPoint(.bottom) // 核心改动:用y方向偏移控制显隐,锚点固定在底部不会出现底部漏空 .offset(y: isOpen ? 0 : contentSize.height) .animation( .spring() .speed(1.25), value: isOpen // 限制动画仅响应isOpen变化,避免其他状态触发异常动画 ) } .edgesIgnoringSafeArea(.bottom) } } // 补充SwiftUI锚点设置扩展 extension View { func anchorPoint(_ anchor: UnitPoint) -> some View { self.transformEffect(.init(translationX: anchor.x * -1, y: anchor.y * -1)) .transformEffect(.init(scaleX: 1, y: 1)) .transformEffect(.init(translationX: anchor.x, y: anchor.y)) } } struct BottomSheetView_Previews: PreviewProvider { static var previews: some View { ZStack { Color.gray.opacity(0.3).edgesIgnoringSafeArea(.all) BottomSheetView(isOpen: .constant(true)) { Text("Bottom Sheet") .frame(maxWidth: .infinity, minHeight: 200.0) .background(Color.red) } } } }
拖拽手势适配说明
后续添加拖拽手势时,只需要把拖拽的偏移量直接叠加到现有offset属性上即可,因为锚点已经固定在底部,无论怎么拖拽都不会出现底部漏空的问题。拖拽结束时只需要判断偏移量阈值,决定是收回弹层还是回弹到展开位置即可。
内容的提问来源于stack exchange,提问作者RPK
相关产品推荐
相关产品推荐

