以编程方式检测 SwiftUI 中的 Tab Bar 或 TabView 高度
Programmatically detect Tab Bar or TabView height in SwiftUI
我有一个 SwiftUI 应用程序,它将有一个浮动的播客播放器,类似于位于选项卡栏上方的 Apple Music 播放器,并且在播放器处于 运行 时持续显示在所有选项卡和视图中。我还没有想出一个很好的方法来定位播放器,使其与标签栏齐平,因为标签栏的高度会根据设备而变化。我发现的主要问题是我必须如何将播放器定位在应用程序根视图中的叠加层或 ZStack 中,而不是在 TabView 本身中。由于我们无法自定义 TabView 布局的视图层次结构,因此无法在 TabBar 本身及其上方视图的内容之间注入视图。我的基本代码结构:
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)
}
})
这里的主要问题是 PlayerContainerView 的位置是用 58 的填充硬编码的,因此它清除了 TabView。如果我可以检测到 TabView 的实际框架高度,我可以针对给定的设备全局调整它,我会没事的。有谁知道如何可靠地做到这一点?或者您是否知道如何将 PlayerContainerView 放置在 TabView 本身中,以便它在切换显示时仅出现在 Home() 视图和 Tab Bar 之间?如有任何反馈,我们将不胜感激。
官方允许并记录了 UIKit 的桥接,因此可以在需要时从那里读取所需的信息。
这是直接从 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)
}
}
}
看来,您需要知道播放器的最大尺寸(标签栏上方 space 的尺寸),而不是标签栏本身的高度。
使用 GeometryReader 和 PreferenceKey 是方便的工具
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()
}
}
在示例中,我在黄色透明矩形上使用 .padding()
缩小了尺寸,以确保不会隐藏任何部分(屏幕外)
如果需要的话,连标签栏的高度都可以计算出来,但我想不出来是为了什么。
根据 user3441734 的回答进行扩展,您应该能够使用此大小来获取 TabView 和内容视图大小之间的差异。这个差异就是玩家需要定位的偏移量。这是一个应该可以工作的重写版本:
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()
}
}
希望我来帮忙还不算太晚。
以下代码是我对这个问题的看法。它适用于所有设备,还可以通过纵向和横向的设备旋转更新高度。
struct TabBarHeighOffsetViewModifier: ViewModifier {
let action: (CGFloat) -> Void
//MARK: this screenSafeArea helps determine the correct tab bar height depending on device version
private let screenSafeArea = (UIApplication.shared.windows.first { [=10=].isKeyWindow }?.safeAreaInsets.bottom ?? 34)
func body(content: Content) -> some View {
GeometryReader { proxy in
content
.onAppear {
let offset = proxy.safeAreaInsets.bottom - screenSafeArea
action(offset)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
let offset = proxy.safeAreaInsets.bottom - screenSafeArea
action(offset)
}
}
}
}
extension View {
func tabBarHeightOffset(perform action: @escaping (CGFloat) -> Void) -> some View {
modifier(TabBarHeighOffsetViewModifier(action: action))
}
}
struct MainTabView: View {
var body: some View {
TabView {
Text("Add the extension on subviews of tabview")
.tabBarHeightOffset { offset in
print("the offset of tabview is -\(offset)")
}
}
}
}
偏移量可以应用于视图以悬停在标签栏上。
这是@Asperi 回答的另一个用途。
我需要根据用户的选择使用切换来更改选项卡栏的背景颜色。 ( Black/Clear )
在选项卡视图中。
我们可以访问用户默认值的存储布尔值。 (他们通过在其他地方使用设置布尔值的切换按钮来选择)
@AppStorage("radioViewImageBGSafeEdges") var showBGUnderTabBar: Bool = true
我们还有一个@State 变量用于 a。 UITabBar
@State private var tabBar_ = UITabBar()
这将加载我们从@Asperi 代码中获得的标签栏。
在我的例子中,只有一个视图需要更改标签栏。其他都保持不变。
所以我只在该选项卡视图下添加修饰符。
RadioView().tag(0).tabItem { Label("Radio", systemImage: "dot.radiowaves.left.and.right") }
.background(Color.black.opacity(0.5))
.background(TabBarAccessor { tabBar in
//==load the tabBar into our star var for use elsewhere
self.tabBar_ = tabBar
//== change colour
tabBar.backgroundColor = showBGUnderTabBar ? UIColor.black : UIColor.clear
})
--
看到我在修饰符里面加了这一行
self.tabBar_ = tabBar
我们现在可以访问标签栏,并且可以在同一标签视图的 .onChange(of:) 修饰符中使用它。
.onChange(of: showBGUnderTabBar) { newValue in
tabBar_.backgroundColor = newValue ? UIColor.black : UIColor.clear
}
现在当状态变量改变时,我们的标签栏颜色会立即改变。
我有一个 SwiftUI 应用程序,它将有一个浮动的播客播放器,类似于位于选项卡栏上方的 Apple Music 播放器,并且在播放器处于 运行 时持续显示在所有选项卡和视图中。我还没有想出一个很好的方法来定位播放器,使其与标签栏齐平,因为标签栏的高度会根据设备而变化。我发现的主要问题是我必须如何将播放器定位在应用程序根视图中的叠加层或 ZStack 中,而不是在 TabView 本身中。由于我们无法自定义 TabView 布局的视图层次结构,因此无法在 TabBar 本身及其上方视图的内容之间注入视图。我的基本代码结构:
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)
}
})
这里的主要问题是 PlayerContainerView 的位置是用 58 的填充硬编码的,因此它清除了 TabView。如果我可以检测到 TabView 的实际框架高度,我可以针对给定的设备全局调整它,我会没事的。有谁知道如何可靠地做到这一点?或者您是否知道如何将 PlayerContainerView 放置在 TabView 本身中,以便它在切换显示时仅出现在 Home() 视图和 Tab Bar 之间?如有任何反馈,我们将不胜感激。
官方允许并记录了 UIKit 的桥接,因此可以在需要时从那里读取所需的信息。
这是直接从 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)
}
}
}
看来,您需要知道播放器的最大尺寸(标签栏上方 space 的尺寸),而不是标签栏本身的高度。
使用 GeometryReader 和 PreferenceKey 是方便的工具
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()
}
}
在示例中,我在黄色透明矩形上使用 .padding()
缩小了尺寸,以确保不会隐藏任何部分(屏幕外)
如果需要的话,连标签栏的高度都可以计算出来,但我想不出来是为了什么。
根据 user3441734 的回答进行扩展,您应该能够使用此大小来获取 TabView 和内容视图大小之间的差异。这个差异就是玩家需要定位的偏移量。这是一个应该可以工作的重写版本:
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()
}
}
希望我来帮忙还不算太晚。 以下代码是我对这个问题的看法。它适用于所有设备,还可以通过纵向和横向的设备旋转更新高度。
struct TabBarHeighOffsetViewModifier: ViewModifier {
let action: (CGFloat) -> Void
//MARK: this screenSafeArea helps determine the correct tab bar height depending on device version
private let screenSafeArea = (UIApplication.shared.windows.first { [=10=].isKeyWindow }?.safeAreaInsets.bottom ?? 34)
func body(content: Content) -> some View {
GeometryReader { proxy in
content
.onAppear {
let offset = proxy.safeAreaInsets.bottom - screenSafeArea
action(offset)
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
let offset = proxy.safeAreaInsets.bottom - screenSafeArea
action(offset)
}
}
}
}
extension View {
func tabBarHeightOffset(perform action: @escaping (CGFloat) -> Void) -> some View {
modifier(TabBarHeighOffsetViewModifier(action: action))
}
}
struct MainTabView: View {
var body: some View {
TabView {
Text("Add the extension on subviews of tabview")
.tabBarHeightOffset { offset in
print("the offset of tabview is -\(offset)")
}
}
}
}
偏移量可以应用于视图以悬停在标签栏上。
这是@Asperi 回答的另一个用途。
我需要根据用户的选择使用切换来更改选项卡栏的背景颜色。 ( Black/Clear )
在选项卡视图中。
我们可以访问用户默认值的存储布尔值。 (他们通过在其他地方使用设置布尔值的切换按钮来选择)
@AppStorage("radioViewImageBGSafeEdges") var showBGUnderTabBar: Bool = true
我们还有一个@State 变量用于 a。 UITabBar
@State private var tabBar_ = UITabBar()
这将加载我们从@Asperi 代码中获得的标签栏。
在我的例子中,只有一个视图需要更改标签栏。其他都保持不变。
所以我只在该选项卡视图下添加修饰符。
RadioView().tag(0).tabItem { Label("Radio", systemImage: "dot.radiowaves.left.and.right") }
.background(Color.black.opacity(0.5))
.background(TabBarAccessor { tabBar in
//==load the tabBar into our star var for use elsewhere
self.tabBar_ = tabBar
//== change colour
tabBar.backgroundColor = showBGUnderTabBar ? UIColor.black : UIColor.clear
})
--
看到我在修饰符里面加了这一行
self.tabBar_ = tabBar
我们现在可以访问标签栏,并且可以在同一标签视图的 .onChange(of:) 修饰符中使用它。
.onChange(of: showBGUnderTabBar) { newValue in
tabBar_.backgroundColor = newValue ? UIColor.black : UIColor.clear
}
现在当状态变量改变时,我们的标签栏颜色会立即改变。