🔥

SwiftUI Sheet 自适应高度:让弹窗完美贴合内容尺寸

(更新于 )

核心摘要:SwiftUI 原生的 presentationDetents 通常只支持 .medium.large。要实现 Sheet 高度严格由内容动态决定,我们需要结合 GeometryReader 动态测量尺寸,并将计算出的高度传递给 .height() 修饰符。

在 SwiftUI 开发中,让 Sheet 的高度根据内容自动调整(Auto-sizing)是一个常见需求。虽然 iOS 16 引入了 presentationDetents,但默认缺乏 “fitContent” 选项。本文介绍如何通过简单的封装实现这一效果,并解决高度变化时的动画抖动问题。

核心实现逻辑

  1. 测量:在内容视图的 .background 中使用 GeometryReader 读取实际渲染高度。
  2. 传递:将读取到的高度存储在 @State 中。
  3. 应用:将高度传给 .presentationDetents([.height(currentHeight)])
  4. 刷新:使用 .id() 强制触发布局更新,解决内容变化时的动画问题。

完整代码示例

你可以直接复制以下 modifier 到你的项目中使用:

Swift
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(全屏)模式,避免内容被截断。

Swift
// 伪代码示例
ViewThatFits(in: .vertical) {
    Content() // 尝试完整显示
    ScrollView { Content() } // 空间不足则滚动
}

延伸阅读

相关提示

订阅 Fatbobman 周报

每周精选 Swift 与 SwiftUI 开发技巧,加入众多开发者的行列。

立即订阅