SwiftUI 的新 NavigationStack API 与 UIHostingController 的导航栏不能很好地配合

OMG*_*POP 5 ios swift swiftui

我有一个 UIKit 应用程序,我使用 UIHostingController 将一些屏幕迁移到 SwiftUI。我曾经能够重复使用 UIKit 的相同导航栏。但在切换到 NavigationStack API 后,我无法复制相同的行为。

这是完整的可重现代码。


import UIKit
import SwiftUI

struct ListView: View {
  
  @State var selectedString: String? = nil
  
  var body: some View {
    let details = ["foo", "bar", "baz"]

    // No need to wrap under NavigationView, otherwise will have double Nav Bar
    // This will use the UIKit's nav bar
    List {
      ForEach(details, id: \.self) { detail in
        let destination = Text("This is a detailed page for \(detail)")
          .navigationTitle("Detail page")
        
        NavigationLink(
          detail,
          destination: destination,
          tag: detail,
          selection: $selectedString)
      }
    }
    .navigationTitle("List page")
  }
}

struct ListViewWithNewAPI: View {
  @State var selectedString: String? = nil
  
  var body: some View {
    let details = ["foo", "bar", "baz"]
    
    NavigationStack {
      List(details, id: \.self, selection: $selectedString) { detail in
        NavigationLink(detail, value: detail)
      }
      .navigationDestination(item: $selectedString) { detail in
        Text("This is a detailed page for \(detail)")
          .navigationTitle("Detail page")
      }
      .navigationTitle("List page")
      .navigationBarTitleDisplayMode(.inline)
    }
  }
}

class ViewController: UIViewController {

  @objc
  private func tapButton1() {
    let listVC = UIHostingController(rootView: ListView())
    navigationController?.pushViewController(listVC, animated: true)
  }
  
  @objc
  private func tapButton2() {
    let listVC = UIHostingController(rootView: ListViewWithNewAPI())
    navigationController?.pushViewController(listVC, animated: true)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    
    let button1 = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    button1.backgroundColor = .green
    button1.addTarget(self, action: #selector(tapButton1), for: .touchUpInside)
    view.addSubview(button1)
    
    let button2 = UIButton(frame: CGRect(x: 100, y: 300, width: 100, height: 100))
    button2.backgroundColor = .red
    button2.addTarget(self, action: #selector(tapButton2), for: .touchUpInside)
    view.addSubview(button2)
   
    navigationItem.title = "UIKit title"
  }
}
Run Code Online (Sandbox Code Playgroud)

故事板文件几乎是初始项目,除了我将 VC 嵌入到 nav vc 下。

在此输入图像描述

在上面的代码中,ListView是使用已弃用的 实现的NavigationView,它与UIHostingController. ListViewWithNewAPI是使用新 API 的新实现NavigationStack,我无法复制原始行为。

这是一个比较这两种行为的视频。请使用示例代码并尝试一下,看看我们是否可以使用新的 API 实现原始行为。

在此输入图像描述

Jac*_*sen 2

NavigationStack 取代了 NavigationView。在您的代码中,您将 UIHostingController 推送到导航控制器上。然而,ListViewWithNewAPI 中的视图包含一个 NavigationStack。这意味着您现在有两个级别的“导航堆栈”,从而导致不良行为。

以下是解决该问题的三种不同选项:

  1. 一种选择是删除故事板中的导航控制器。您不能直接在故事板中使用 UIHostingController,因为它使用泛型,但您可以将其包装在(非 UINavigationController)UIViewController 中。或者,您可以完全摆脱故事板并采用 SwiftUI 应用程序生命周期。然后,您可以按原样使用 ListViewWithNewAPI 中的代码。

  2. 您可以继续使用 NavigationLink(destination: label:),它没有被弃用,并且可以继续与 UINavigationController 结合使用。不幸的是,这意味着没有程序化或基于价值的导航。

struct ListViewWithNewAPI: View {
    @State var selectedString: String? = nil
    
    var body: some View {
        let details = ["foo", "bar", "baz"]
        
        List(details, id: \.self, selection: $selectedString) { detail in
            NavigationLink {
                Text("This is a detailed page for \(detail)")
                    .navigationTitle("Detail page")
            } label: {
                Text(detail)
            }
            
        }
            .navigationTitle("List page")
            .navigationBarTitleDisplayMode(.inline)
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 如果您需要保留 UINavigationController (因为其他基于 UIKit 的代码)并且您想要编程/基于值的导航,您可以执行以下操作:

在 SwiftUI 中使用的导航模型

class NavigationModel {
    var navigationController: UINavigationController?
    
    func push<V: View>(@ViewBuilder _ view: () -> V) {
        let vc = UIHostingController(rootView: view())
        navigationController?.pushViewController(vc, animated: true)
    }
}
Run Code Online (Sandbox Code Playgroud)

将 navigationController 添加到模型并将模型提供给您的视图

// property of ViewController
private let navigationModel = NavigationModel()

@objc
private func tapButton2() {
    navigationModel.navigationController = navigationController
    let rootView = ListViewNavigation(navigationModel: navigationModel)
    let hc = UIHostingController(rootView: rootView)
    navigationController?.pushViewController(hc, animated: true)
}
Run Code Online (Sandbox Code Playgroud)

这就是您使用导航模型的方式

struct ListViewNavigation: View {
    let navigationModel: NavigationModel
    
    let details = ["foo", "bar", "baz"]
    
    var body: some View {
        List(details, id: \.self) { detail in
            Button(action: { navigateTo(detail) }) {
                Text("Go to \(detail)")
            }
        }
    }
    
    func navigateTo(_ detail: String) {
        navigationModel.push {
            Text("This is a detail page for \(detail)")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)