Различные виды панели навигации для разных вкладок в TabView в SwiftUI

У меня есть TabView, на котором есть 3 вкладки («Обязательства», «Входы/выходы» и «Активы»). На каждой вкладке есть NavigationView. Я хочу, чтобы для каждого из них был разный внешний вид навигационной панели (красная тема для пассивов, белая для входа/выхода и зеленая тема для активов).

Я могу без труда установить цвета фона навигационных панелей, используя что-то вроде этого:

.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.liabilitiesnav, for: .navigationBar)

Это позволяет мне установить только цвет фона, но мне нужно иметь возможность изменять цвета других элементов на панели навигации. Кнопками, которые я добавляю на панель инструментов, я могу управлять, явно устанавливая их цвета, так что это не проблема. Но заголовком навигации, текстом и значком кнопки «Назад» можно управлять только с помощью глобальной функциональности UINavigationBar.appearance(). Но мне не нужен глобальный внешний вид, я хочу настроить разные вкладки с разным внешним видом. Это действительно важно, потому что мой AccentColor — темно-зеленый, и хотя этот зеленый цвет хорошо смотрится на кнопке «Назад» и элементах панели инструментов на вкладке «Активы»… он ужасно зеленый на красном на вкладке «Обязательства». Вот почему мне нужно иметь возможность управлять ими отдельно.

Я пробовал использовать механику .onAppear { }, чтобы попытаться изменить глобальный внешний вид, чтобы он соответствовал текущей вкладке всякий раз, когда эта вкладка появляется. Например, на вкладке «Обязательства» у меня есть:

NavigationView {
    List {
        // stuff
    }
    .onAppear {
        // tried it here...
        NavHelper.useRedAppearance()
    }
} 
.onAppear {
    // and tried it here as well
    NavHelper.useRedAppearance()
}

Однако, похоже, это рассинхронизируется. Все начнется правильно (Обязательства = красный и Активы = зеленый), но когда я щелкаю вперед и назад между вкладками «Обязательства» и «Активы», обновления, кажется, не синхронизируются, и иногда «Обязательства» отображаются зеленым, а иногда «Активы» отображаются красным. . Я добавил несколько операторов печати в код onAppear и увидел, что useRedAppearance() вызывается, когда я нажимаю на вкладку «Обязательства», а useGreenAppearance() вызывается, когда я нажимаю на вкладку «Активы»... но цвета не обязательно обновляются. каждый раз... и таким образом рассинхронизировались.

Вот частичная вставка NavHelper на случай, если я делаю что-то не так:

class NavHelper {   

    static func useRedAppearance() {
        let textcolor = UIColor.moneyred
        let backgroundcolor = UIColor.liabilitiesnav
        
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = backgroundcolor
        appearance.titleTextAttributes = [.foregroundColor: textcolor]
        appearance.largeTitleTextAttributes = [.foregroundColor: textcolor]
        
        let buttonAppearance = UIBarButtonItemAppearance()
        buttonAppearance.normal.titleTextAttributes = [.foregroundColor: textcolor]
        
        let image = UIImage(systemName: "chevron.backward")!.withTintColor(textcolor, renderingMode: .alwaysOriginal)
        appearance.setBackIndicatorImage(image, transitionMaskImage: image)
        
        appearance.buttonAppearance = buttonAppearance
        appearance.backButtonAppearance = buttonAppearance
        
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().compactAppearance = appearance
        UINavigationBar.appearance().compactScrollEdgeAppearance = appearance
        
    }

}

Как я могу либо (а) надежно переключать глобальный внешний вид вперед и назад, не теряя синхронизации, либо (б) индивидуально настраивать представления на разных вкладках, чтобы у них была только фиксированная цветовая тема?


2
50
1

Ответ:

Решено

Цвета не синхронизируются, поскольку API UIAppearance не может изменить внешний вид существующих представлений. Иногда панель навигации перемещается в окно до вызова useRedAppearance, поэтому на нее это не влияет.

Если вы просто хотите контролировать цвет кнопки «Назад», вы можете использовать tint:

NavigationStack {
    SomeView()
        // 'tint' on the NavigationStack also affects tints of other views in the navigation stack
        // so we apply another 'tint' here, to change the tint back
        .tint(Color.accent)
}
// this sets the back button color to red
.tint(.red)

Если вы также хотите изменить цвет заголовка, поместите цветной Text в качестве элемента .principal панели инструментов.

.toolbar {
    ToolbarItem(placement: .principal) {
        Text("Liabilities")
            .foregroundStyle(.yellow)
    }
}

Альтернативно, вы можете использовать UIViewControllerRepresentable, чтобы получить UINavigationController и установить для него цвета.

struct NavigationAppearance: UIViewControllerRepresentable {
    var textColor: UIColor
    var backgroundColor: UIColor
    
    class VC: UIViewController {
        var textColor: UIColor = .clear
        var backgroundColor: UIColor = .clear
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            updateBarAppearance()
        }
        
        func updateBarAppearance() {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = backgroundColor
            appearance.titleTextAttributes = [.foregroundColor: textColor]
            appearance.largeTitleTextAttributes = [.foregroundColor: textColor]
            
            let buttonAppearance = UIBarButtonItemAppearance()
            buttonAppearance.normal.titleTextAttributes = [.foregroundColor: textColor]
            
            let image = UIImage(systemName: "chevron.backward")!.withTintColor(textColor, renderingMode: .alwaysOriginal)
            appearance.setBackIndicatorImage(image, transitionMaskImage: image)
            
            appearance.buttonAppearance = buttonAppearance
            appearance.backButtonAppearance = buttonAppearance
            
            navigationController?.navigationBar.standardAppearance = appearance
            navigationController?.navigationBar.scrollEdgeAppearance = appearance
            navigationController?.navigationBar.compactAppearance = appearance
            navigationController?.navigationBar.compactScrollEdgeAppearance = appearance
            
        }
    }
    
    func makeUIViewController(context: Context) -> VC {
        VC()
    }
    
    func updateUIViewController(_ uiViewController: VC, context: Context) {
        uiViewController.textColor = textColor
        uiViewController.backgroundColor = backgroundColor
    }
}

Все, что вам нужно сделать, это поместить это где-нибудь в иерархию представлений, например. background

NavigationStack {
    SomeView()
        .navigationTitle("Assets")
        .navigationBarTitleDisplayMode(.inline)
        .background {
            NavigationAppearance(textColor: .systemRed, backgroundColor: .systemGreen)
        }
}