SwiftUI中如何实现显示在TabBar上方的Bottom Sheet
实现不覆盖TabBar的SwiftUI Bottom Sheet
核心原因
系统默认的.sheet是模态弹窗,层级高于整个TabView,所以会覆盖TabBar。要实现需求,必须让弹窗的层级落在TabView的子页面范围内。
方案一:自定义可拖拽底部弹窗(推荐)
通过自定义组件,将弹窗嵌入到每个Tab的子视图中,确保弹窗处于当前页面的上层,TabBar始终在底部显示。
自定义BottomSheet组件代码
// 自定义可拖拽的BottomSheet组件 struct BottomSheetView<Content: View>: View { @Binding var isPresented: Bool let content: Content @State private var offsetY: CGFloat = UIScreen.main.bounds.height init(isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) { self._isPresented = isPresented self.content = content() } var body: some View { ZStack(alignment: .bottom) { // 半透明背景,点击关闭 if isPresented { Color.black.opacity(0.3) .ignoresSafeArea() .onTapGesture { withAnimation(.easeOut) { isPresented = false offsetY = UIScreen.main.bounds.height } } // 弹窗内容区域 VStack { content } .background(Color.white) .cornerRadius(16, corners: [.topLeft, .topRight]) .offset(y: offsetY) // 添加拖拽手势 .gesture( DragGesture() .onChanged { value in offsetY = max(value.translation.height, 0) } .onEnded { value in let threshold = UIScreen.main.bounds.height * 0.5 withAnimation(.easeOut) { if value.translation.height > threshold { isPresented = false offsetY = UIScreen.main.bounds.height } else { offsetY = 0 } } } ) .onAppear { withAnimation(.easeOut) { offsetY = 0 } } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } } // 扩展:设置指定圆角 extension View { func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { clipShape(RoundedCorner(radius: radius, corners: corners)) } } struct RoundedCorner: Shape { var radius: CGFloat = .infinity var corners: UIRectCorner = .allCorners func path(in rect: CGRect) -> Path { let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) return Path(path.cgPath) } }
使用示例
struct ContentView: View { @State private var isBottomSheetPresented = false var body: some View { TabView { // 第一个Tab页面 VStack { Button("打开底部弹窗") { isBottomSheetPresented = true } .padding() } .tabItem { Label("首页", systemImage: "house") } // 将弹窗作为当前页面的overlay添加 .overlay( BottomSheetView(isPresented: $isBottomSheetPresented) { VStack(spacing: 20) { Text("自定义底部弹窗") .font(.headline) Text("该弹窗不会覆盖TabBar") .font(.subheadline) Button("关闭") { isBottomSheetPresented = false } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } .padding() .frame(height: 300) } ) // 第二个Tab页面 Text("我的页面") .tabItem { Label("我的", systemImage: "person") } } } }
方案二:在Tab子视图中使用系统Sheet(仅iOS 16+)
如果仅需适配iOS 16及以上版本,可以将系统.sheet添加到Tab的子视图中,并设置弹窗高度,虽然系统Sheet仍会覆盖TabBar,但关闭后直接回到当前Tab页面,体验接近需求:
struct ContentView: View { @State private var isBottomSheetPresented = false var body: some View { TabView { VStack { Button("打开底部弹窗") { isBottomSheetPresented = true } .padding() } .tabItem { Label("首页", systemImage: "house") } .sheet(isPresented: $isBottomSheetPresented) { VStack(spacing: 20) { Text("Tab内系统弹窗") .font(.headline) Button("关闭") { isBottomSheetPresented = false } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } .presentationDetents([.medium]) // 设置弹窗为中等高度 } Text("我的页面") .tabItem { Label("我的", systemImage: "person") } } } }
总结
- 自定义弹窗组件是最兼容、灵活的方案,支持全iOS版本,且能完全控制弹窗不覆盖TabBar。
- 系统Sheet方案仅适用于iOS 16+,仍会覆盖TabBar,但交互流程更贴近系统原生体验。
内容的提问来源于stack exchange,提问作者etete
相关产品推荐
相关产品推荐

