You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 18:15:11