Programmatically detect Tab Bar or TabView height in SwiftUI
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)
}
}
}
It seems, that you need to know the maximal size of player (size of space above tab bar), not height of tab bar self.
Using GeometryReader and PreferenceKey are the handy tool for that
import Combine
struct Size: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
struct HomeView: View {
let txt: String
var body: some View {
GeometryReader { proxy in
Text(self.txt).preference(key: Size.self, value: [proxy.frame(in: CoordinateSpace.global)])
}
}
}
struct ContentView: View {
@State var playerFrame = CGRect.zero
var body: some View {
TabView {
HomeView(txt: "Hello").tabItem {
Image(systemName: "house")
Text("A")
}.border(Color.green).tag(1)
HomeView(txt: "World!").tabItem {
Image(systemName: "house")
Text("B")
}.border(Color.red).tag(2)
HomeView(txt: "Bla bla").tabItem {
Image(systemName: "house")
Text("C")
}.border(Color.blue).tag(3)
}
.onPreferenceChange(Size.self, perform: { (v) in
self.playerFrame = v.last ?? .zero
print(self.playerFrame)
})
.overlay(
Color.yellow.opacity(0.2)
.frame(width: playerFrame.width, height: playerFrame.height)
.position(x: playerFrame.width / 2, y: playerFrame.height / 2)
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In the example I reduce the size with .padding()
on yellow transparent rectangle, to be sure no part could be hidden (out of screen)
Even the height of tab bar could be calculated, if necessary, but I am not able to imagine for what.
Expanding upon the answer from user3441734, you should be able to use this Size to get the difference between TabView and content view sizes. That difference is then the offset where the player needs to be positioned. Here is a rewritten version that should work:
import SwiftUI
struct InnerContentSize: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
struct HomeView: View {
let txt: String
var body: some View {
GeometryReader { proxy in
Text(self.txt)
.preference(key: InnerContentSize.self, value: [proxy.frame(in: CoordinateSpace.global)])
}
}
}
struct PlayerView: View {
var playerOffset: CGFloat
var body: some View {
VStack(alignment: .leading) {
Spacer()
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 55, height: 55)
.cornerRadius(8.0)
Text("Name of really cool song")
Spacer()
Image(systemName: "play.circle")
.font(.title)
}
.padding(.horizontal)
Spacer()
}
.background(Color.pink.opacity(0.2))
.frame(height: 70)
.offset(y: -playerOffset)
}
}
struct ContentView: View {
@State private var playerOffset: CGFloat = 0
var body: some View {
GeometryReader { geometry in
TabView {
HomeView(txt: "Foo")
.tag(0)
.tabItem {
Image(systemName: "sun.min")
Text("Sun")
}
HomeView(txt: "Bar")
.tag(1)
.tabItem {
Image(systemName: "moon")
Text("Moon")
}
HomeView(txt: "Baz")
.tag(2)
.tabItem {
Image(systemName: "sparkles")
Text("Stars")
}
}
.ignoresSafeArea()
.onPreferenceChange(InnerContentSize.self, perform: { value in
self.playerOffset = geometry.size.height - (value.last?.height ?? 0)
})
.overlay(PlayerView(playerOffset: playerOffset), alignment: .bottom)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}