【问题标题】:SwiftUI override navigation bar appearance in detail viewSwiftUI 在详细视图中覆盖导航栏外观
【发布时间】:2020-09-13 12:25:56
【问题描述】:

我有一个超级简单的 SwiftUI 主从应用程序:

import SwiftUI

struct ContentView: View {
    @State private var imageNames = [String]()

    var body: some View {
        NavigationView {
            MasterView(imageNames: $imageNames)
                .navigationBarTitle(Text("Master"))
                .navigationBarItems(
                    leading: EditButton(),
                    trailing: Button(
                        action: {
                            withAnimation {
                                // simplified for example
                                self.imageNames.insert("image", at: 0)
                            }
                        }
                    ) {
                        Image(systemName: "plus")
                    }
                )
        }
    }
}

struct MasterView: View {
    @Binding var imageNames: [String]

    var body: some View {
        List {
            ForEach(imageNames, id: \.self) { imageName in
                NavigationLink(
                    destination: DetailView(selectedImageName: imageName)
                ) {
                    Text(imageName)
                }
            }
        }
    }
}

struct DetailView: View {

    var selectedImageName: String

    var body: some View {
        Image(selectedImageName)
    }
}

我还在 SceneDelegate 上为导航栏的颜色设置外观代理”

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        let navBarAppearance = UINavigationBarAppearance()
        navBarAppearance.configureWithOpaqueBackground()
        navBarAppearance.shadowColor = UIColor.systemYellow
        navBarAppearance.backgroundColor = UIColor.systemYellow
        navBarAppearance.shadowImage = UIImage()
        UINavigationBar.appearance().standardAppearance = navBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance

        // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView()

        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

现在,我想做的是在详细视图出现时将导航栏的背景颜色更改为清除。我仍然想要那个视图中的后退按钮,所以隐藏导航栏并不是一个理想的解决方案。我还希望更改仅适用于 Detail 视图,因此当我弹出该视图时,外观代理应该接管,如果我推送到另一个控制器,那么外观代理也应该接管。

我一直在尝试各种事情: - 更改didAppear 的外观代理 - 将详细视图包装在 UIViewControllerRepresentable 中(成功有限,我可以进入导航栏并更改其颜色,但由于某种原因,导航控制器不止一个)

在 SwiftUI 中是否有直接的方法来做到这一点?

【问题讨论】:

    标签: swiftui swiftui-navigationlink uinavigationbarappearance


    【解决方案1】:

    我更喜欢为此使用 ViewModifer。下面是我的自定义 ViewModifier

    struct NavigationBarModifier: ViewModifier {
    
    var backgroundColor: UIColor?
    
    init(backgroundColor: UIColor?) {
        self.backgroundColor = backgroundColor
    
        let coloredAppearance = UINavigationBarAppearance()
        coloredAppearance.configureWithTransparentBackground()
        coloredAppearance.backgroundColor = backgroundColor
        coloredAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
        coloredAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
    
        UINavigationBar.appearance().standardAppearance = coloredAppearance
        UINavigationBar.appearance().compactAppearance = coloredAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = coloredAppearance
        UINavigationBar.appearance().tintColor = .white
    }
    
    func body(content: Content) -> some View {
        ZStack{
            content
            VStack {
                GeometryReader { geometry in
                    Color(self.backgroundColor ?? .clear)
                        .frame(height: geometry.safeAreaInsets.top)
                        .edgesIgnoringSafeArea(.top)
                    Spacer()
                }
            }
        }
    }}
    

    您也可以为您的栏使用不同的文本颜色和色调颜色来初始化它,我现在添加了静态颜色。

    你可以从任何地方调用这个修饰符。你的情况

        NavigationLink(
    destination: DetailView(selectedImageName: imageName)
        .modifier(NavigationBarModifier(backgroundColor: .green))
    

    )

    下面是截图。

    【讨论】:

    • 感谢您的回复@user832 - 这是一个非常酷的解决方案。如果您希望所有导航栏的颜色一致并且只覆盖应用中的其中一个,您是否仍需要在所有视图上添加修饰符?
    • @KerrM 我通常将此修饰符应用于我的 NavigationView,因此它对于该导航视图内的所有视图保持一致,这很有意义,因为导航视图包含我的其他视图。但是如果你将它应用到一个独立的视图上,你将不得不在所有的视图上添加这个修饰符。如果你检查这个修饰符的 body 属性,你会发现它只会改变它所应用的内容。
    • 感谢@user832 的快速回复。我一定是做错了什么,因为当我将视图修饰符应用于 NavigationView 时,它不会覆盖导航栏,它只会覆盖状态栏。此外,当导航栏从大模式变为内联模式时(即滚动时在列表上),背景颜色不会随导航栏缩小。我认为这是一个很好的解决方案,我将继续研究如何使其与这两种用途一起使用。
    • @KerrM 您可以在问题中添加更新的代码,我很乐意提供帮助。因为 SwiftUI 中有很多无法解释的概念。
    • 嘿@user832,感谢您的帮助。我在这里上传了项目:github.com/kerrmarin/swiftui-navigation
    【解决方案2】:

    在我看来,这是 SwiftUI 中最直接的解决方案。

    问题:framework adds back buttom in the DetailView 解决方案:Custom back button and nav bar are rendered

    struct DetailView: View {
    var selectedImageName: String
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        CustomizedNavigationController(imageName: selectedImageName) { backButtonDidTapped in
            if backButtonDidTapped {
                presentationMode.wrappedValue.dismiss()
            }
        } // creating customized navigation bar
        .navigationBarTitle("Detail")
        .navigationBarHidden(true) // Hide framework driven navigation bar
     }
    }
    

    如果框架驱动的导航栏没有隐藏在详细视图中,我们会得到两个这样的导航栏:double nav bars

    使用 UINavigationBar.appearance() 在我们想要在弹出窗口中同时显示这些 Master 和 Detail 视图的情况下是非常不安全的。我们应用程序中的所有其他导航栏都有可能获得与详细信息视图相同的导航栏配置。

    struct CustomizedNavigationController: UIViewControllerRepresentable {
    let imageName: String
    var backButtonTapped: (Bool) -> Void
    
    class Coordinator: NSObject {
        var parent: CustomizedNavigationController
        var navigationViewController: UINavigationController
        
        init(_ parent: CustomizedNavigationController) {
            self.parent = parent
            let navVC = UINavigationController(rootViewController: UIHostingController(rootView: Image(systemName: parent.imageName).resizable()
                                                                                        .aspectRatio(contentMode: .fit)
                                                                                        .foregroundColor(.blue)))
            navVC.navigationBar.isTranslucent = true
            navVC.navigationBar.tintColor = UIColor(red: 41/255, green: 159/244, blue: 253/255, alpha: 1)
            navVC.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.red]
            navVC.navigationBar.barTintColor = .yellow
            navVC.navigationBar.topItem?.title = parent.imageName
            self.navigationViewController = navVC
        }
        
        @objc func backButtonPressed(sender: UIBarButtonItem) {
            self.parent.backButtonTapped(sender.isEnabled)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> UINavigationController {
        // creates custom back button 
        let navController = context.coordinator.navigationViewController
        let backImage = UIImage(systemName: "chevron.left")
        let backButtonItem = UIBarButtonItem(image: backImage, style: .plain, target: context.coordinator, action: #selector(Coordinator.backButtonPressed))
        navController.navigationBar.topItem?.leftBarButtonItem = backButtonItem
        return navController
    }
    
    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
        //Not required
    }
    }
    

    这里是link查看完整代码。

    【讨论】:

      【解决方案3】:

      我最终创建了一个自定义包装器,它显示了一个未附加到当前 UINavigationController 的 UINavigationBar。是这样的:

      final class TransparentNavigationBarContainer<Content>: UIViewControllerRepresentable where Content: View {
      
          private let content: () -> Content
      
          init(content: @escaping () -> Content) {
              self.content = content
          }
      
          func makeUIViewController(context: Context) -> UIViewController {
              let controller = TransparentNavigationBarViewController()
      
              let rootView = self.content()
                  .navigationBarTitle("", displayMode: .automatic) // needed to hide the nav bar
                  .navigationBarHidden(true)
              let hostingController = UIHostingController(rootView: rootView)
              controller.addContent(hostingController)
              return controller
          }
      
          func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
      }
      
      final class TransparentNavigationBarViewController: UIViewController {
      
          private lazy var navigationBar: UINavigationBar = {
              let navBar = UINavigationBar(frame: .zero)
              navBar.translatesAutoresizingMaskIntoConstraints = false
      
              let navigationItem = UINavigationItem(title: "")
      
              navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "chevron.left"),
                                                                 style: .done,
                                                                 target: self,
                                                                 action: #selector(back))
      
              let appearance = UINavigationBarAppearance()
              appearance.backgroundImage = UIImage()
              appearance.shadowImage = UIImage()
              appearance.backgroundColor = .clear
              appearance.configureWithTransparentBackground()
              navigationItem.largeTitleDisplayMode = .never
              navigationItem.standardAppearance = appearance
              navBar.items = [navigationItem]
              navBar.tintColor = .white
              return navBar
          }()
      
          override func viewDidLoad() {
              super.viewDidLoad()
              self.view.translatesAutoresizingMaskIntoConstraints = false
      
              self.view.addSubview(self.navigationBar)
      
              NSLayoutConstraint.activate([
                  self.navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor),
                  self.navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor),
                  self.navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
              ])
          }
      
          override func didMove(toParent parent: UIViewController?) {
              super.didMove(toParent: parent)
      
              guard let parent = parent else {
                  return
              }
      
              NSLayoutConstraint.activate([
                  parent.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                  parent.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                  parent.view.topAnchor.constraint(equalTo: self.view.topAnchor),
                  parent.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
              ])
          }
      
          @objc func back() {
              self.navigationController?.popViewController(animated: true)
          }
      
          fileprivate func addContent(_ contentViewController: UIViewController) {
              contentViewController.willMove(toParent: self)
              self.addChild(contentViewController)
              contentViewController.view.translatesAutoresizingMaskIntoConstraints = false
              self.view.addSubview(contentViewController.view)
      
              NSLayoutConstraint.activate([
                  self.view.topAnchor.constraint(equalTo: contentViewController.view.safeAreaLayoutGuide.topAnchor),
                  self.view.bottomAnchor.constraint(equalTo: contentViewController.view.bottomAnchor),
                  self.navigationBar.leadingAnchor.constraint(equalTo: contentViewController.view.leadingAnchor),
                  self.navigationBar.trailingAnchor.constraint(equalTo: contentViewController.view.trailingAnchor)
              ])
      
              self.view.bringSubviewToFront(self.navigationBar)
          }
      }
      
      

      还有一些改进需要做,例如显示自定义导航栏按钮、支持“滑动返回”以及其他一些事情。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2022-11-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-22
        • 2020-11-20
        • 1970-01-01
        相关资源
        最近更新 更多