SwiftUI 打造系统级 Bottom Sheet 交互

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

这类交互体验。

而动态高度计算,则是实现高级交互的关键。

相关推荐
文件夹__iOS1 天前
Swift 5.9 被严重低估的特性:参数包,一次性干掉重复泛型重载
ios·swiftui·swift
东坡肘子3 天前
让 AI 从称手到称心 -- 肘子的 Swift 周报 #134
人工智能·swiftui·swift
东坡肘子11 天前
Swift 并发正被更广泛地接纳 -- 肘子的 Swift 周报 #133
人工智能·swiftui·swift
文件夹__iOS14 天前
SwiftUI 核心选型:class + ObservableObject VS struct + @State
ios·swiftui·swift
Wenzar_15 天前
# 发散创新:SwiftUI 中状态管理的深度实践与重构艺术 在 SwiftUI 的世界里,**状态驱动 UI 是核心哲学**。但随
java·python·ui·重构·swiftui
大熊猫侯佩16 天前
GeometryReader 生存指南(下集):与恶魔共舞——陷阱、禁忌与最终救赎
swiftui·performance·layout·frame·stack·geometryreader·preferencekey
大熊猫侯佩17 天前
别被系统绑架:SwiftUI List 替换背后的底层逻辑
swiftui·swift·apple
东坡肘子18 天前
从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132
人工智能·swiftui·swift