在 UIKit 中自定义和调整 Sheet 尺寸
Customize and resize sheets in UIKit
2021年6月10日
一句话判断
UISheetPresentationController 的 detent 系统让你可以精确控制 Sheet 的每一个尺寸——半屏、全屏、或者任意自定义高度,用户可以丝滑地在它们之间拖拽切换。
这场 Session 讲了什么
iOS 15 引入的 UISheetPresentationController 是 Sheet 交互的重大升级。之前的 UIViewController modal 展示只有两种模式:全屏(.fullScreen)和底部弹出(.pageSheet)。现在你可以创建多级尺寸的 Sheet,用户通过拖拽在各级之间切换。
核心概念是 detent(锚点)。每个 detent 定义了 Sheet 的一个「停靠高度」。系统预设了 .medium()(大约屏幕一半)和 .large()(几乎全屏)。你还可以创建自定义 detent 来指定精确的高度。
Session 展示了几个实际应用场景:地图 app 的地点详情 Sheet(先显示概要,拖上去显示完整信息),音乐 app 的播放队列 Sheet(先显示当前歌曲,拖上去显示整个列表),以及表单的「确认」Sheet。
另外还讨论了 grabber(拖拽把手)的定制、Sheet 圆角的设置、以及在 detent 变化时如何同步更新内容。
值得深挖的点
自定义 Detent 的计算
UISheetPresentationController.Detent.custom 允许你根据 Sheet 的宽度动态计算高度。这对于在不同设备上保持一致的 UI 比例非常有用。比如你希望 Sheet 始终露出顶部导航栏的 44pt,可以这样计算:height = window.height - 44。
Sheet 与 ScrollView 的嵌套
当 Sheet 内容包含 ScrollView 时,拖拽手势可能冲突。系统会自动处理大部分情况:在 Sheet 的最高 detent 时,拖拽会滚动 ScrollView;在非最高 detent 时,拖拽会调整 Sheet 尺寸。但如果你有自定义的 pan gesture recognizer,可能需要手动处理优先级。
代码片段
// 创建多级尺寸的 Sheet
class DetailViewController: UIViewController {
var tableView: UITableView!
var sheetPresentationController: UISheetPresentationController?
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
func setupSheet() {
sheetPresentationController = presentationController as? UISheetPresentationController
sheetPresentationController?.detents = [.medium(), .large()]
sheetPresentationController?.prefersGrabberVisible = true
sheetPresentationController?.prefersScrollingExpandsWhenScrolledToEdge = false
sheetPresentationController?.selectedDetentIdentifier = .medium
sheetPresentationController?.delegate = self
// 自定义圆角和 grabber 颜色
sheetPresentationController?.grabberTopInset = 6
}
}
extension DetailViewController: UISheetPresentationControllerDelegate {
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
_ sheetPresentationController: UISheetPresentationController
) {
let isMedium = sheetPresentationController.selectedDetentIdentifier == .medium
UIView.animate(withDuration: 0.25) {
// 根据当前 detent 调整 UI
self.tableView.isScrollEnabled = !isMedium
}
}
}
// 自定义 detent 高度
func presentCustomSheet() {
let vc = InfoViewController()
if let sheet = vc.sheetPresentationController {
let smallDetent = UISheetPresentationController.Detent.custom { context in
// 计算自定义高度:屏幕高度的 1/3
return context.maximumDetentValue * 0.33
}
let mediumDetent = UISheetPresentationController.Detent.custom { context in
// 精确显示 3 行内容的高度
let rowHeight: CGFloat = 60
let headerHeight: CGFloat = 44
let padding: CGFloat = 20
return headerHeight + rowHeight * 3 + padding
}
sheet.detents = [smallDetent, mediumDetent, .large()]
sheet.prefersGrabberVisible = true
sheet.selectedDetentIdentifier = .medium
}
present(vc, animated: true)
}
// 半屏 Sheet 适合作为快捷操作面板
func presentQuickActions() {
let vc = QuickActionsViewController()
if let sheet = vc.sheetPresentationController {
sheet.detents = [.medium()]
sheet.prefersGrabberVisible = true
sheet.preferredCornerRadius = 20
// 点击外部区域不自动 dismiss
sheet.largestUndimmedDetentIdentifier = .medium
}
present(vc, animated: true)
}
最佳实践
不要在 .medium() detent 下放太多内容。medium 大约只有屏幕高度的一半,内容溢出时用户需要拖拽到 .large() 才能看到。建议 medium 只放概要信息(标题、关键数据),完整内容放在 large。
如果你需要在 detent 变化时执行动画,在 sheetPresentationControllerDidChangeSelectedDetentIdentifier 回调中触发。系统已经完成了 Sheet 的尺寸动画,你只需要更新内容的可见性和布局。
还有什么值得关注
prefersEdgeAttachedInCompactHeight在 iPhone 横屏时让 Sheet 贴在屏幕边缘而不是居中。widthFollowsPreferredContentSizeWhenEdgeAttached控制 Sheet 是否跟随preferredContentSize的宽度。- 在 iPad 上,Sheet 会自动变成 Popover(如果空间允许)。你可以通过
adaptivePresentationDelegate来自定义这个行为。