【问题标题】:Programmatically detect Tab Bar or TabView height in SwiftUI以编程方式检测 SwiftUI 中的 Tab Bar 或 TabView 高度
【发布时间】:2020-01-29 15:03:11
【问题描述】:

我有一个 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 之间?任何反馈将不胜感激。

【问题讨论】:

  • 你试过 GeometryReader 了吗?
  • 是的,但我不知道如何定位 TabView...或者至少我无法找到一个相关的高度值,该值实际上对应于该特定标签栏的绘制高度设备。
  • @eResourcesInc 我试过了,你可以在我的回答中看到它是如何工作的......

标签: swift xcode swiftui ios13 xcode11


【解决方案1】:

由于官方允许并记录到 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)
        }
    }
}

【讨论】:

  • 这可行,但它给我的价值是 iPhone 11 Max Pro 的 83。实际距离(至少在填充中)是 60 像素。我认为你的解决方案给了我到边缘的距离,加上不安全的区域,或者类似的东西。问题是我必须根据 SwiftUI 的边界在应用程序中设置空间,这意味着我不能使用这个精确的计算,除非我知道安全区域和非安全区域之间的设备特定差异。在这种情况下,它是 23 像素,但在其他情况下,它会有所不同。任何想法如何处理?
  • 此外,如果您以编程方式计算同一设备的安全区域插入,则给出的值为 34。但是,83 - 34 不等于 60。所以我只是继续不知道如何以编程方式实际获得该数字,尽管您的回答确实回答了我所说的问题。
  • 是的,一开始我有点怀疑。花了一些时间,但我找到了适用于所有设备和方向的可用解决方案。如果您愿意,请参阅我的答案中的解决方案。
  • @eResourcesInc 解决方案存在,请参阅我的答案(即使我迟到了)
  • 由于某种原因,在 iPadOS 中使用它会使标签栏消失(透明)
【解决方案2】:

看来,您需要知道播放器的最大尺寸(标签栏上方的空间大小),而不是标签栏自身的高度。

使用 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() 减小大小,以确保没有任何部分可以隐藏(屏幕外)

如果有必要,甚至可以计算标签栏的高度,但我无法想象。

【讨论】:

  • 这是解决等式反部分的好方法,非常有用。我已经使用上述答案在短期内“解决”了我的问题,但这看起来是一种很好的、​​稳健的方式来处理未来的任务..
  • 如何在另一个 View 中读取 Size 结构体?
  • 为什么 v.last 在我的测试中为零?
【解决方案3】:

扩展来自 user3441734 的答案,您应该能够使用此 Size 来获取 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()
  }
}

【讨论】:

  • 为什么 value.last 在我的测试中为零?
【解决方案4】:

希望我帮上忙还为时不晚。 以下代码是我对这个问题的看法。它适用于所有设备,还可以通过设备旋转来更新纵向和横向的高度。

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 { $0.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)")
                }
        }
    }
}

偏移量可以应用于视图以悬停在标签栏上。

【讨论】:

    猜你喜欢
    • 2023-03-25
    • 2020-05-22
    • 2020-04-08
    • 1970-01-01
    • 2019-12-08
    • 1970-01-01
    • 1970-01-01
    • 2022-11-10
    • 2014-07-05
    相关资源
    最近更新 更多