SwiftUI 中 Presentation 系列 API 全解析:打造系统级 Bottom Sheet 交互
在 iOS 16 之后,SwiftUI 对 Sheet(底部弹窗)的支持越来越完善。
以前我们如果想实现类似:
- Apple Music
- 地图
- 查找
- 钱包
- 健身
这种「可拖拽、可半屏、可全屏」的 Bottom Sheet,往往需要:
- UIKit + UISheetPresentationController
- 第三方库
- 大量手势逻辑
但现在,SwiftUI 已经提供了一整套 presentation 系列 API。
本文结合一个「Wallet 卡片动画」案例,深入介绍:
- presentationDetents
- presentationBackgroundInteraction
- interactiveDismissDisabled
- presentationBackground
- 动态高度 Sheet 的实现思路
最终实现一个真正接近系统原生体验的 Bottom Sheet。
一、基础 Sheet
最基础的 SwiftUI Sheet:
swift
.sheet(isPresented: $showSheet) {
DetailView()
}
或者:
swift
.sheet(item: $selectedCard) { card in
DetailView(card: card)
}
这里的 item: 版本特别适合:
- 列表详情
- 卡片点击
- 数据驱动导航
比如:
swift
@State private var selectedCard: Card?
.sheet(item: $selectedCard) { card in
TransactionsSheetView(card: card)
}
当 selectedCard != nil 时自动弹出。
二、presentationDetents:控制 Sheet 高度
这是最核心的 API。
1. 系统默认高度
swift
.presentationDetents([.medium, .large])
效果:
- .medium 半屏
- .large 全屏
用户可以拖拽切换。
三、自定义高度
真正强大的地方来了。
你的代码中:
swift
.presentationDetents([
.height(minSheetHeight),
.height(maxSheetHeight)
])
这里使用了:
swift
.height(CGFloat)
也就是说:
SwiftUI 允许你完全自定义 Sheet 高度。
四、动态高度 Bottom Sheet
你的代码:
swift
let spacing: CGFloat = 20
let minSheetHeight: CGFloat =
info.containerSize.height
- info.minY
- (220 + spacing)
let maxSheetHeight: CGFloat =
info.containerSize.height
- info.minY
+ 15
这是一个非常经典的:
「根据当前界面位置动态计算 Sheet 高度」
的实现。
五、为什么这样计算?
你的界面结构:
Plain
卡片区域
↓
Sheet 从底部弹出
目标:
当点击卡片后:
- Sheet 的顶部
- 正好贴在卡片底部
因此:
swift
屏幕高度
- 卡片顶部位置
- 卡片高度
就能得到:
「Sheet 初始高度」
六、onGeometryChange 的作用
你的代码里:
swift
.onGeometryChange(for: CGFloat.self) {
$0.frame(in: .global).minY
} action: { newValue in
info.minY = newValue - info.safeArea.top
}
这是 iOS 18 新增的 API。
作用:
实时监听 View 的几何信息变化。
这里记录的是:
swift
当前 ScrollView 顶部距离屏幕顶部的位置
后面动态计算 Sheet 高度时会用到。
七、presentationBackgroundInteraction
这是很多人忽略,但体验极其重要的 API。
你的代码:
swift
.presentationBackgroundInteraction(
.enabled(upThrough: .height(maxSheetHeight))
)
含义:
当 Sheet 处于指定高度时, 允许与后面的页面交互。
八、不加它会怎样?
默认情况下:
Sheet 出现后:
后面的内容:
- 无法点击
- 无法滚动
- 无法交互
即使 Sheet 只是半屏。
九、加上后的效果
你的效果会变成:
当 Sheet 是:
swift
.height(maxSheetHeight)
以内时:
后面的卡片:
- 仍然可以滚动
- 可以响应交互
这和:
- Apple Maps
- Find My
- Apple Music
的体验完全一致。
十、interactiveDismissDisabled
代码:
swift
.interactiveDismissDisabled()
作用:
禁止用户下滑关闭 Sheet。
十一、为什么需要它?
因为你的界面:
swift
selectedCard = card
会触发:
- 卡片位移动画
- 缩放动画
- 堆叠动画
如果用户直接下滑关闭:
状态可能不同步。
因此:
你改成:
swift
Button("Close") {
selectedCard = nil
}
统一通过业务逻辑关闭。
这是非常推荐的做法。
十二、presentationBackground
代码:
swift
.presentationBackground(schemeBackground)
作用:
设置 Sheet 背景。
例如:
swift
.presentationBackground(.ultraThinMaterial)
可以实现:
毛玻璃效果。
十三、适配深色模式
你的代码:
swift
var schemeBackground: Color {
return colorScheme == .dark ? .black : .white
}
然后:
swift
.presentationBackground(schemeBackground)
这是一种非常实用的方式。
因为:
系统默认 Sheet 背景:
- 有时偏灰
- 有时带 Material
- 不一定符合设计稿
自定义背景能统一视觉。
十四、系统 Bottom Sheet 的核心思想
其实 Apple 的 Bottom Sheet 本质就是:
text
一个支持多高度停靠点(Detents)的容器
用户拖动时:
Sheet 会在:
- medium
- large
- 自定义高度
之间切换。
而:
swift
presentationDetents
就是 SwiftUI 对这个能力的封装。
十五、完整代码示例
结合你的实现:
swift
.sheet(item: $selectedCard) { card in
let spacing: CGFloat = 20
let minSheetHeight =
info.containerSize.height
- info.minY
- (220 + spacing)
let maxSheetHeight =
info.containerSize.height
- info.minY
+ 15
TransactionsSheetView(card: card)
.presentationDetents([
.height(minSheetHeight),
.height(maxSheetHeight)
])
.presentationBackgroundInteraction(
.enabled(upThrough: .height(maxSheetHeight))
)
.interactiveDismissDisabled()
.presentationBackground(schemeBackground)
}
十六、推荐组合
1. 普通页面
swift
.presentationDetents([.medium, .large])
2. Apple Music 风格
swift
.presentationDetents([
.fraction(0.2),
.medium,
.large
])
3. 地图类应用
swift
.presentationBackgroundInteraction(.enabled)
4. 强业务流程
swift
.interactiveDismissDisabled()
比如:
- 支付
- 登录
- 引导流程
十七、iOS 版本支持
| API | 最低版本 |
|---|---|
| presentationDetents | iOS 16 |
| presentationBackground | iOS 16 |
| presentationBackgroundInteraction | iOS 16.4 |
| onGeometryChange | iOS 18 |
十八、总结
SwiftUI 的 presentation 系列 API 已经足够强大。
现在实现系统级 Bottom Sheet:
基本不再需要 UIKit。
尤其:
swift
presentationDetents
配合:
swift
presentationBackgroundInteraction
已经能够实现:
- Apple Maps
- Wallet
- Music
- Find My
这类交互体验。
而动态高度计算,则是实现高级交互的关键。