swiftxcodeswiftuiios13xcode11

Programmatically detect Tab Bar or TabView height in SwiftUI


I have a SwiftUI app that will have a floating podcast player, similar to the Apple Music player that sits just above the Tab Bar and persists across all tabs and views while the player is running. I have not figured out a good way to position the player so that it is flush above the Tab Bar, since the Tab Bar height changes based on device. The main issue I've found is just how I have to position the player in an overlay or ZStack in the root view of my app, instead of within the TabView itself. Since we can't customize the view hierarchy of the TabView layout, there is no way to inject a view in-between the TabBar itself and the content of the view above it. My basic code structure:

TabView(selection: $appState.selectedTab){
  Home()
  .tabItem {
    VStack {
        Image(systemName: "house")
        Text("Home")
    }
  }
  ...
}.overlay(
  VStack {
    if(audioPlayer.isShowing) {
      Spacer()
      PlayerContainerView(player: audioPlayer.player)
      .padding(.bottom, 58)
      .transition(.moveAndFade)
    }
  })

The main issue here is that the location of the PlayerContainerView is hard-coded with a padding of 58 so that it clears the TabView. If I could detect the actual frame height of the TabView, I could globally adjust this for the given device and I would be fine. Does anybody know how to do this reliably? Or do you have any idea how I could place the PlayerContainerView within the TabView itself so that it simply appears BETWEEN the Home() view and the Tab Bar when it is toggled to show? Any feedback would be appreciated.


Solution

  • As bridge to UIKit is officially allowed and documented, it is possible to read needed information from there when needed.

    Here is possible approach to read tab bar height directly from UITabBar

    // Helper bridge to UIViewController to access enclosing UITabBarController
    // and thus its UITabBar
    struct TabBarAccessor: UIViewControllerRepresentable {
        var callback: (UITabBar) -> Void
        private let proxyController = ViewController()
    
        func makeUIViewController(context: UIViewControllerRepresentableContext<TabBarAccessor>) ->
                                  UIViewController {
            proxyController.callback = callback
            return proxyController
        }
        
        func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TabBarAccessor>) {
        }
        
        typealias UIViewControllerType = UIViewController
    
        private class ViewController: UIViewController {
            var callback: (UITabBar) -> Void = { _ in }
    
            override func viewWillAppear(_ animated: Bool) {
                super.viewWillAppear(animated)
                if let tabBar = self.tabBarController {
                    self.callback(tabBar.tabBar)
                }
            }
        }
    }
    
    // Demo SwiftUI view of usage
    struct TestTabBar: View {
        var body: some View {
            TabView {
                Text("First View")
                    .background(TabBarAccessor { tabBar in
                        print(">> TabBar height: \(tabBar.bounds.height)")
                        // !! use as needed, in calculations, @State, etc.
                    })
                    .tabItem { Image(systemName: "1.circle") }
                    .tag(0)
                Text("Second View")
                    .tabItem { Image(systemName: "2.circle") }
                    .tag(1)
            }
        }
    }