SwiftUI:实现可拖动底部表单的ScrollView方案咨询
实现SwiftUI可拖动ScrollView抽屉的最简方案
我刚好研究过这种抽屉交互——苹果Maps、Music这类App里的设计核心是区分拖动行为是滚动内部内容还是拖动整个容器:当ScrollView处于顶部时,拖动会带动抽屉;当ScrollView已经滚动过内容,拖动只会滚动列表。下面是用SwiftUI原生API实现的最简方案,不用依赖第三方库,也不用复杂的UIKit包装:
核心思路
- 用
DragGesture监听抽屉的拖动动作 - 通过滚动偏移判断ScrollView是否在顶部(iOS 17+用原生的
scrollPosition,低版本可以用自定义ScrollView监听) - 只有当ScrollView在顶部时,才响应抽屉的拖动;否则让ScrollView自己处理滚动
完整代码示例(iOS 17+)
import SwiftUI struct DraggableScrollDrawer: View { // 控制抽屉的垂直偏移量 @State private var drawerOffset: CGFloat = 0 // 记录ScrollView的滚动偏移(iOS 17+原生支持) @State private var scrollOffset: CGFloat = 0 // 可自定义的抽屉高度范围 private let minDrawerHeight: CGFloat = 200 private let maxDrawerHeight: CGFloat = UIScreen.main.bounds.height - 100 var body: some View { ZStack { // 背景层(替换成你的主内容,比如地图) Color.blue.opacity(0.3) .ignoresSafeArea() // 可拖动的抽屉容器 VStack(spacing: 0) { // 顶部拖动指示器,引导用户交互 RoundedRectangle(cornerRadius: 3) .frame(width: 40, height: 6) .foregroundColor(.gray) .padding(.vertical, 8) // 带滚动内容的ScrollView ScrollView { LazyVStack(spacing: 16) { ForEach(1...50, id: \.self) { index in Text("列表项 \(index)") .frame(maxWidth: .infinity) .padding() .background(.white) .cornerRadius(8) .padding(.horizontal) } } .scrollTargetLayout() // 配合scrollPosition使用 } .scrollPosition(id: $scrollOffset) // 监听滚动偏移 .scrollIndicators(.hidden) } .background(.white) .cornerRadius(16, corners: [.topLeft, .topRight]) // 限制抽屉在最小/最大高度之间移动 .offset(y: max(minDrawerHeight, maxDrawerHeight - drawerOffset)) .gesture( DragGesture() .onChanged { value in // 只有当ScrollView在顶部时,才允许拖动抽屉 if scrollOffset <= 0 { drawerOffset = value.translation.height } } .onEnded { value in // 拖动结束自动吸附到最近的高度 let velocity = value.velocity.height let midPoint = (maxDrawerHeight - minDrawerHeight)/2 if velocity < -1000 || drawerOffset > midPoint { withAnimation(.spring()) { drawerOffset = maxDrawerHeight - minDrawerHeight } } else { withAnimation(.spring()) { drawerOffset = 0 } } } ) } } } #Preview { DraggableScrollDrawer() }
关键细节说明
- 交互逻辑:当ScrollView没有滚动(
scrollOffset <= 0)时,拖动操作会带动抽屉;如果ScrollView已经向下滚动,拖动只会滚动内部列表,和苹果官方App的行为完全一致。 - 自动吸附:拖动结束时,根据拖动速度和偏移距离,自动让抽屉吸附到最小或最大高度,避免停在中间的尴尬状态。
- 兼容性方案(iOS 16及以下):如果需要支持旧版本系统,可以用
UIViewRepresentable包装UIScrollView来监听滚动偏移,核心代码如下:
// 兼容iOS 16的自定义ScrollView,用于监听滚动偏移 struct ScrollViewWithOffset: UIViewRepresentable { @Binding var offset: CGFloat func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator scrollView.showsVerticalScrollIndicator = false return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UIScrollViewDelegate { var parent: ScrollViewWithOffset init(_ parent: ScrollViewWithOffset) { self.parent = parent } func scrollViewDidScroll(_ scrollView: UIScrollView) { parent.offset = scrollView.contentOffset.y } } }
把原代码中的ScrollView替换成这个自定义组件,绑定scrollOffset就能实现相同的交互逻辑。
内容的提问来源于stack exchange,提问作者blrsk
相关产品推荐
相关产品推荐

