核心摘要:SwiftUI 原生的 presentationDetents 通常只支持 .medium 或 .large。要实现 Sheet 高度严格由内容动态决定,我们需要结合 GeometryReader 动态测量尺寸,并将计算出的高度传递给 .height() 修饰符。
在 SwiftUI 开发中,让 Sheet 的高度根据内容自动调整(Auto-sizing)是一个常见需求。虽然 iOS 16 引入了 presentationDetents,但默认缺乏 “fitContent” 选项。本文介绍如何通过简单的封装实现这一效果,并解决高度变化时的动画抖动问题。
核心实现逻辑
- 测量:在内容视图的
.background中使用GeometryReader读取实际渲染高度。 - 传递:将读取到的高度存储在
@State中。 - 应用:将高度传给
.presentationDetents([.height(currentHeight)])。 - 刷新:使用
.id()强制触发布局更新,解决内容变化时的动画问题。
完整代码示例
你可以直接复制以下 modifier 到你的项目中使用:
extension View {
func adaptiveSheet<Content: View>(isPresent: Binding<Bool>, @ViewBuilder sheetContent: () -> Content) -> some View {
modifier(AdaptiveSheetModifier(isPresented: isPresent, sheetContent))
}
}
struct AdaptiveSheetModifier<SheetContent: View>: ViewModifier {
@Binding var isPresented: Bool
@State private var subHeight: CGFloat = 0
var sheetContent: SheetContent
init(isPresented: Binding<Bool>, @ViewBuilder _ content: () -> SheetContent) {
_isPresented = isPresented
sheetContent = content()
}
func body(content: Content) -> some View {
content
.background(
sheetContent // 在此获取尺寸,防止初次弹出抖动
.background(
GeometryReader { proxy in
Color.clear
.task(id: proxy.size.height) {
subHeight = proxy.size.height
}
}
)
.hidden()
)
.sheet(isPresented: $isPresented) {
sheetContent
.presentationDetents([.height(subHeight)])
}
.id(subHeight)
}
}
struct ContentView: View {
@State var show = false
@State var height: CGFloat = 250
var body: some View {
List {
Button("Pop Sheet") {
height = 250
show.toggle()
}
Button("Pop ScrollView Sheet") {
height = 1000
show.toggle()
}
}
.adaptiveSheet(isPresent: $show) {
ViewThatFits(in: .vertical) {
SheetView(height: height)
ScrollView {
SheetView(height: height)
}
}
}
}
}
struct SheetView: View {
let height: CGFloat
var body: some View {
Text("Hi")
.frame(maxWidth: .infinity, minHeight: height)
.presentationBackground(.orange)
}
}
关键技术点解析
1. 消除高度抖动 (Flickering)
在 Sheet 初次展开时,由于高度计算需要一帧的时间,可能会出现先全屏再缩回的视觉抖动。
解决方案:可以在 presentationDetents 中提供一个初始的预估高度(如 .medium),或者在高度计算完成前隐藏内容(opacity(0))。但在 Swift 6 / iOS 26 环境下,系统的渲染管线已优化,直接绑定 .height 通常表现良好。
2. 动态内容刷新
如果 Sheet 已经打开,且内部内容发生变化(例如用户点击了“展开更多”),我们需要 Sheet 平滑地长高。
这必须配合 .id(height) 或在 onPreferenceChange 中使用 withAnimation,否则 presentationDetents 可能不会立即响应新的高度值。
3. 结合 ViewThatFits
对于极度动态的内容,建议结合 ViewThatFits 使用,确保在小屏幕设备上如果内容过高,自动切换回 .large(全屏)模式,避免内容被截断。
// 伪代码示例
ViewThatFits(in: .vertical) {
Content() // 尝试完整显示
ScrollView { Content() } // 空间不足则滚动
}