SwiftUI实现不同自定义高度堆叠Sheet的问题:避免下层Sheet被上层拉伸
我完全理解你的需求——想要堆叠两个不同高度的Sheet,下层展示功能预览(半高),上层作为付费墙(1/3高度),同时保留Sheet自带的滑动关闭特性,而不是用Overlay替代。你遇到的问题确实是SwiftUI默认Sheet交互的一个小坑:当你在子Sheet设置自定义Detent时,父Sheet会自动被拉伸到和子Sheet一样的高度,这显然破坏了你想要的分层展示效果。
下面给你两种可行的解决方案,分别对应SwiftUI原生实现和兼容旧版本的UIKit桥接方案:
方案一:SwiftUI原生实现(iOS 16+)
从iOS 16开始,SwiftUI提供了presentationBackgroundInteraction修饰符,我们可以用它来禁用上层Sheet对下层Sheet的尺寸影响,让下层Sheet保持自己的自定义高度。
首先先定义我们需要的自定义Detent:
extension PresentationDetent { // 屏幕半高的自定义Detent static let half = Self.height(UIScreen.main.bounds.height / 2) // 屏幕1/3高度的自定义Detent static let oneThird = Self.height(UIScreen.main.bounds.height / 3) }
然后修改你的ContentView代码,关键是给上层Sheet添加.presentationBackgroundInteraction(.disabled):
struct ContentView: View { @State private var showingFirst = false @State private var showingSecond = false var body: some View { VStack { Button("Show First Sheet") { showingFirst = true } } .sheet(isPresented: $showingFirst) { VStack { Button("Show Second Sheet") { showingSecond = true } .sheet(isPresented: $showingSecond) { Text("Second Sheet (One-Third Height)") .presentationDetents([.oneThird]) // 核心:禁用背景交互,阻止上层Sheet影响下层Sheet的尺寸 .presentationBackgroundInteraction(.disabled) // 显示滑动指示器,保留Sheet的原生交互提示 .presentationDragIndicator(.visible) } } .presentationDetents([.half]) .presentationDragIndicator(.visible) } } }
这个方法的原理是:.presentationBackgroundInteraction(.disabled)会告诉系统,上层Sheet的背景区域(也就是下层Sheet的部分)不响应交互,这样系统就不会自动调整下层Sheet的高度来适配上层,完美保留了两个Sheet的独立自定义高度,同时滑动关闭的特性也完全保留。
方案二:UIKit桥接实现(兼容iOS 15及以下)
如果需要支持iOS 15或更早版本,我们可以通过包装UISheetPresentationController来实现自定义Sheet行为,绕过SwiftUI的默认限制:
import SwiftUI import UIKit // 自定义Sheet的UIKit包装器 struct CustomSheet<Content: View>: UIViewControllerRepresentable { let content: Content let detents: [UISheetPresentationController.Detent] let keepsParentSheetSize: Bool init(detents: [UISheetPresentationController.Detent], keepsParentSheetSize: Bool, @ViewBuilder content: () -> Content) { self.detents = detents self.keepsParentSheetSize = keepsParentSheetSize self.content = content() } func makeUIViewController(context: Context) -> UIHostingController<Content> { UIHostingController(rootView: content) } func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) { guard let sheet = uiViewController.sheetPresentationController else { return } sheet.detents = detents sheet.prefersGrabberVisible = true // 显示滑动指示器 if keepsParentSheetSize { // 禁用滚动扩展,避免父Sheet被拉伸 sheet.prefersScrollingExpandsWhenScrolledToEdge = false // 不设置最大未遮罩Detent,让父Sheet保持原高度 sheet.largestUndimmedDetentIdentifier = nil // 确保父Sheet区域仍然可交互(可选,根据需求调整) sheet.presentationController?.containerView?.superview?.isUserInteractionEnabled = true } } } // 使用示例 struct ContentView: View { @State private var showingFirst = false @State private var showingSecond = false var body: some View { VStack { Button("Show First Sheet") { showingFirst = true } } .sheet(isPresented: $showingFirst) { VStack { Button("Show Second Sheet") { showingSecond = true } .sheet(isPresented: $showingSecond) { CustomSheet( detents: [.height(UIScreen.main.bounds.height/3)], keepsParentSheetSize: true ) { Text("Second Sheet (One-Third Height)") } } } .presentationDetents([.height(UIScreen.main.bounds.height/2)]) .presentationDragIndicator(.visible) } } }
这个方案通过直接操作UIKit的UISheetPresentationController,手动配置Sheet的行为,确保上层Sheet弹出时不会影响下层Sheet的尺寸,同时也保留了滑动关闭的原生特性。
备注:内容来源于stack exchange,提问作者Jim Margolis




