SwiftUI实现类似Find My的Sheet显示在Tab Bar上方且不覆盖
SwiftUI实现Sheet显示在TabBar上方且不覆盖它
SwiftUI默认的.sheet是系统级模态弹窗,会覆盖整个窗口(包括底部的TabBar)。要实现弹窗显示在TabBar上方且保留TabBar可见,你可以采用以下两种纯SwiftUI或结合UIKit的方案:
方案1:纯SwiftUI自定义弹窗
通过自定义弹窗组件,将其放在TabView的上层区域,底部留出TabBar的空间,同时模拟系统Sheet的交互效果。
修改后的完整代码:
import SwiftUI struct ContentView: View { @State private var showSheet = true @State private var sheetOffset = CGSize.zero // TabBar默认高度,也可通过UIKit动态获取 private let tabBarHeight: CGFloat = 49 var body: some View { GeometryReader { geo in ZStack(alignment: .bottom) { TabView { HomeView(showSheet: $showSheet) .tabItem { Label("Home", systemImage: "house") } Text("Second Tab") .tabItem { Label("Second", systemImage: "2.circle") } } // 自定义弹窗 if showSheet { SheetContent() .frame(height: geo.size.height - tabBarHeight) .background(.white) .cornerRadius(16, corners: [.topLeft, .topRight]) .offset(y: sheetOffset.height) .gesture( DragGesture() .onChanged { sheetOffset = $0.translation } .onEnded { value in if value.translation.height > 50 { showSheet = false } sheetOffset = .zero } ) .transition(.move(edge: .bottom)) } } } .animation(.easeInOut, value: showSheet) } } struct HomeView: View { @Binding var showSheet: Bool var body: some View { VStack { Button("Show TabView Sheet") { showSheet.toggle() } } } } struct SheetContent: View { var body: some View { VStack(spacing: 20) { // 顶部拖拽指示器,模拟系统Sheet样式 RoundedRectangle(cornerRadius: 3) .frame(width: 40, height: 6) .foregroundColor(.gray) .padding(.top, 10) Text("First Tab Content") Text("More Content in the Sheet") Spacer() } .padding() } } #Preview { ContentView() }
方案说明:
- 用
ZStack将TabView和自定义弹窗分层,弹窗固定在底部,高度为屏幕总高度减去TabBar高度 - 添加拖拽手势支持下拉关闭弹窗,顶部的指示器还原系统Sheet的交互体验
- 通过
transition和animation实现平滑的显示/隐藏动画
方案2:结合UIKit调整弹窗层级(兼容iOS 15+)
如果需要更贴近系统Sheet的原生行为(比如支持presentationDetents),可以通过UIViewControllerRepresentable包装UIKit的弹窗控制器,指定弹窗的显示区域避开TabBar。
完整代码示例:
import SwiftUI import UIKit struct SheetHostingController<Content: View>: UIViewControllerRepresentable { var content: Content @Binding var isPresented: Bool func makeUIViewController(context: Context) -> UIHostingController<Content> { let controller = UIHostingController(rootView: content) controller.modalPresentationStyle = .pageSheet if let sheet = controller.sheetPresentationController { sheet.detents = [.medium(), .large()] // 设置弹窗的显示区域,避开TabBar sheet.sourceView = context.coordinator.tabBarController?.view let tabBarHeight = context.coordinator.tabBarController?.tabBar.frame.height ?? 49 sheet.sourceRect = CGRect( x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height - tabBarHeight ) } return controller } func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) { if isPresented, uiViewController.presentingViewController == nil { context.coordinator.parentViewController?.present(uiViewController, animated: true) } else if !isPresented { uiViewController.dismiss(animated: true) } } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject { var parent: SheetHostingController weak var parentViewController: UIViewController? weak var tabBarController: UITabBarController? init(parent: SheetHostingController) { self.parent = parent super.init() DispatchQueue.main.async { self.parentViewController = UIApplication.shared.windows.first?.rootViewController self.tabBarController = self.parentViewController as? UITabBarController } } } } struct ContentView: View { @State private var showSheet = true var body: some View { TabView { HomeView(showSheet: $showSheet) .tabItem { Label("Home", systemImage: "house") } Text("Second Tab") .tabItem { Label("Second", systemImage: "2.circle") } } .background(SheetHostingController(content: SheetContent(), isPresented: $showSheet)) } } struct HomeView: View { @Binding var showSheet: Bool var body: some View { VStack { Button("Show TabView Sheet") { showSheet.toggle() } } } } struct SheetContent: View { var body: some View { VStack(spacing: 20) { Text("First Tab Content") Text("More Content in the Sheet") Spacer() } .padding() } } #Preview { ContentView() }
方案说明:
- 通过
UISheetPresentationController设置弹窗的显示边界,确保弹窗底部停在TabBar上方 - 保留了系统Sheet的
detents(中等/大尺寸)等原生特性 - 利用
Coordinator获取当前的TabBarController,动态计算TabBar高度
内容的提问来源于stack exchange,提问作者Sheehan Munim
相关产品推荐
相关产品推荐

