当前位置:首页>iOSAPP>iOS 16 之后再写高德地图抽屉效果,不用自己撸手势,系统 API 轻松搞定

iOS 16 之后再写高德地图抽屉效果,不用自己撸手势,系统 API 轻松搞定

  • 2026-08-07 04:33:06
iOS 16 之后再写高德地图抽屉效果,不用自己撸手势,系统 API 轻松搞定

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

昨天写一个外卖类 App,产品同学甩过来一张 Apple Maps 的截图:「就要这种底部抽屉,能拖到一半,能拖到顶,下面地图还能滑」。

我第一反应是:又得自己撸 DragGesture 了。结果摸了一圈发现,SwiftUI 从 iOS 16 开始原生就支持 Bottom Sheet,核心就靠一个 modifier —— presentationDetents。配合 interactiveDismissDisabled 和 presentationBackgroundInteraction,三行代码就出效果,手势冲突也不用自己处理。

这篇把这套写法、坑和边界一次说清楚。

先看效果,再看为什么要用原生

苹果地图、高德、滴滴打车,这类应用底部那块卡片,业内一般叫可拖动底部抽屉。它有几条硬性要求:

  • 至少三个停靠点:折叠(只露一点)、中等高度、几乎全屏
  • 不能被用户一不小心滑没了
  • 抽屉拉起来的时候,下面的地图还能拖、还能点
  • 抽屉里如果是 ScrollView,要能跟外层拖拽手势协调,不能打架

以前在 UIKit 里实现这套,要么自己接 UIPanGestureRecognizer,要么用 UISheetPresentationController 配一堆 delegate。SwiftUI 把这事抹平了,下面这套写法基本是目前的最优解。

三个 modifier 解决主要问题

直接上代码,场景换成「附近的餐厅列表」:

struct NearbyRestaurantsView: View {
    @State private var sheetVisible = true

    var body: some View {
        MapContainer()
            .sheet(isPresented: $sheetVisible) {
                ScrollView {
                    RestaurantList()
                }
                .interactiveDismissDisabled()
                .presentationDetents([.height(80), .medium, .large])
                .presentationBackgroundInteraction(
                    .enabled(upThrough: .large)
                )
                .presentationDragIndicator(.visible)
            }
    }
}

就这点东西。每个 modifier 单独说一下,因为每一行都在解决一个非常具体的问题。

.presentationDetents([.height(80), .medium, .large])

定义抽屉的停靠点。这里给了三档:固定 80 点高度(只露搜索框和拖拽条)、屏幕一半、几乎全屏。用户拖动时会自动吸附到最近的那一档,不用自己写 withAnimation,也不用自己算速度。

.interactiveDismissDisabled()

这行很关键。默认 sheet 往下一拉就关掉了,但地图类应用底部抽屉是「常驻」的,不能消失。加了这个 modifier 之后,用户怎么拖都不会让 sheet 消失,最低只会停在最小那档。

.presentationBackgroundInteraction(.enabled(upThrough: .large))

这个我第一次看到的时侯愣了一下。它的意思是:当 sheet 处于 .large 及以下任意停靠点时,底层视图(也就是地图)依然可以接收手势。不加这一行,sheet 一旦显示,下面的地图就完全锁死了,那这个 UI 就没什么意义了。

ScrollView 和拖拽手势会不会打架?

不会,系统帮你处理掉了。

具体行为:当 ScrollView 滚到顶部、你还继续往下拉,系统自动把手势交给 sheet 本身,把整个抽屉往下拽到下一个停靠点。反过来,抽屉拖到 .large 之后再继续向上滑,手势又交还给 ScrollView 去滚动内容。

这套交接逻辑在 UIKit 里要写一堆 gestureRecognizer:shouldRecognizeSimultaneouslyWith:,SwiftUI 这边是默认行为,零代码。

自定义停靠点和拖拽条

如果 .medium 和 .large 不够用,presentationDetents 还接受比例和具体数值:

.presentationDetents([
    .height(80),
    .fraction(0.4),     // 屏幕高度的 40%
    .fraction(0.85)
])

presentationDragIndicator(.visible) 会在抽屉顶部画一条小灰条,提示用户「这里可以拖」。不是非加不可,但加了之后辨识度高一截,建议默认开着。

这套方案的问题在哪?

SwiftUI 原生 sheet 有一个绕不开的限制:它会盖住整个视图层级,包括 TabBar。

也就是说,如果你的 App 是带底部 TabBar 的结构,sheet 弹出来之后会把 TabBar 也遮住。Apple Maps 自己那个抽屉之所以能飘在 TabBar 上方又不挡住 TabBar,是因为用的是私有 API,不是我们能用的 .sheet。

如果产品需求是「抽屉飘在 TabBar 之上」,只能自己用 GeometryReader + DragGesture + offset 撸一个,或者上 UIViewControllerRepresentable 桥 UISheetPresentationController 来配。但只要你的页面是单页结构,.sheet 配三行 modifier 完全够用,没必要自己造轮子。

最后

我自己最近做的一个工具类 App 已经把这套搬上去了,大概写了十几行就跑通,比预想的简单很多。

你觉得这套方案怎么样?评论聊聊。

这里每天分享一个 iOS 的新知识,快来关注我吧

最新文章

随机文章