Программное определение панели вкладок или высоты TabView в SwiftUI
У меня есть приложение SwiftUI, которое будет иметь плавающий проигрыватель подкастов, похожий на проигрыватель Apple Music, который находится прямо над панелью вкладок и сохраняется во всех вкладках и представлениях во время работы проигрывателя. Я не придумал, как правильно расположить проигрыватель так, чтобы он был на одном уровне над панелью вкладок, поскольку высота панели вкладок изменяется в зависимости от устройства. Основная проблема, которую я обнаружил, заключается в том, как я должен позиционировать проигрыватель в оверлее или ZStack в корневом представлении моего приложения, а не в самом TabView. Поскольку мы не можем настроить иерархию представлений макета TabView, нет способа вставить представление между самим Tab Bar и содержимым представления над ним. Моя основная структура кода:
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() и панелью вкладок, когда он переключается на отображение? Любая обратная связь будет оценена.
6 ответов
Поскольку мост к 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)
}
}
}
Кажется, что вам нужно знать максимальный размер плеера (размер пространства над панелью вкладок), а не высоту панели вкладок.
Использование 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 { $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)")
}
}
}
}
Смещение можно применять к представлениям, чтобы навести курсор на панель вкладок.
Ответ @Asperi работает, но, к сожалению, не поддерживает изменение ориентации устройства + есть проблема со смещением.
Вот что нужно изменить:
Изменить@State private var offset:CGFloat = 0
значение по этому:
offset = tabBar.bounds.height-tabBar.safeAreaInsets.bottom
А затем добавьте это в ViewController :
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
DispatchQueue.main.async {
if let tabBar = self.tabBarController {
self.callback(tabBar.tabBar)
}
}
}
Это еще одно использование ответа @Asperi.
Мне нужно было изменить цвет фона панели вкладок в зависимости от выбора пользователя с помощью переключателя. (черный/прозрачный)
В представлении вкладок.
У нас есть доступ к сохраненному bool для пользовательских значений по умолчанию. (они выбирают, используя кнопку переключения в другом месте, которая устанавливает логическое значение)
@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
}
Теперь, когда переменная состояния изменится, цвет панели вкладок сразу же изменится.