带有路径的 NavigationStack 在 UIHostingController 中无法正常工作

NavigationStack with path doesn't work properly inside UIHostingController

提问人:lazarevzubov 提问时间:11/17/2023 更新时间:11/17/2023 访问量:21

问:

当 s 被包装在 中时,通过 的导航不起作用:即使将值附加到 ,也不会推送相应的视图。UIHostingControllerrootViewNavigationStackNavigationStackpathNavigationPath

奇怪的是,如果用非空初始化,它会正确显示堆栈:NavigationStackpath

Incorrect behavior of NavigationStack inside UIHostingController

(在动画中,带有“首先显示”按钮的屏幕是一个 ,它以模式方式显示一个包装在(带有“下一步打开”按钮的视图)中的 SwiftUI 视图。“打开下一个”修改视图,尝试推送另一个带有“下一步”文本标签的 SwiftUI 视图。UIViewControllerUIHostingControllerpath

这是一个最小的可重现示例,去除了所有不相关的代码:

class HomeViewController: UIViewController {

  class NavigationModel: ObservableObject {

    enum Path {
      case second
    }

    @Published var navigationPath = NavigationPath()

  }

  @ObservedObject private var navigationModel = NavigationModel()

  override func viewDidLoad() {
    super.viewDidLoad()

    view.backgroundColor = .white

    let button = UIButton(type: .system)
    button.translatesAutoresizingMaskIntoConstraints = false
    button.setTitle("Show first", for: .normal)
    button.addTarget(self, action: #selector(presentSecondViewController), for: .touchUpInside)

    view.addSubview(button)
    NSLayoutConstraint.activate([button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor)])
  }

  @objc func presentSecondViewController() {
    let view = NavigationStack(path: $navigationModel.navigationPath) {
      Button("Open next") { self.navigationModel.navigationPath.append(NavigationModel.Path.second) }
        .navigationDestination(for: NavigationModel.Path.self) {
          switch $0 {
            case .second:
              SecondView()
          }
        }
    }

    let vc = UIHostingController(rootView: view)
    present(vc, animated: true)
  }

}

struct SecondView: View {
  var body: some View {
    Text("Next")
  }
}

但是,同样的方法在纯 SwiftUI 中也有效:如果根视图以模式方式呈现另一个包装在 中的视图,则对相应视图的所有修改都会导致预期行为:NavigationStackpath

Correct behavior of NavigationStack in pure SwiftUI

下面是工作示例的代码:

@main struct SwiftUIPlaygroundApp: App {

  class NavigationModel: ObservableObject {

    enum Path {
      case second
    }

    @Published var navigationPath = NavigationPath()

  }

  var body: some Scene {
    WindowGroup {
      Button("Show first") { firstShown = true }
        .sheet(isPresented: $firstShown) {
          NavigationStack(path: $navigationModel.navigationPath) {
            Button("Open next") { navigationModel.navigationPath.append(NavigationModel.Path.second) }
              .navigationDestination(for: NavigationModel.Path.self) {
                switch $0 {
                  case .second:
                    SecondView()
                }
              }
          }
        }
    }
  }
  @State var firstShown = false
  @ObservedObject var navigationModel = NavigationModel()

}

struct SecondView: View {
  var body: some View {
    Text("Next")
  }
}

我怎样才能让它在里面正常工作?UIHostingController

任何指导都是值得赞赏的。

iOS SwiftUI UIKit的

评论

1赞 lorem ipsum 11/18/2023
尝试更改为 SwiftUI 包装器在 UiKit 中没有任何作用,如果你需要绑定,你需要进入一个绑定在 UIKit 中不起作用,@ObservedObjectletSwiftUI.View
0赞 lazarevzubov 11/20/2023
进入 SwiftUI 视图确实可以使它工作。但它也迫使我在视图内部移动,这打破了将视图与导航分开的想法。(问题中的视图控制器只是演示该问题的最小示例。实际上,我有一个协调器类,而不是视图控制器,它提供了上面的 UIKit 的视图控制器,并通过操作其 的 。@ObservedObjectnavigationDestination(for:)UIHostingControllerUIHostingControllerNavigationStackpath
0赞 lorem ipsum 11/20/2023
您将无法做到这一点,UIKit 和 SwiftUI 是 2 个不同的框架,它们的工作方式非常不同,SwiftUI 依赖于 UIKit 无法使用的 DynamicProperty。你可以使用 UIKit 的 UINavigationController 来呈现一系列 UIHostingController,也可以坚持使用纯 SwiftUI 的做事方式。如果你决定将两个框架强行结合在一起,你很可能会遇到问题。
0赞 lazarevzubov 11/20/2023
当然,这并不是说我想强迫它。只是我正在开发一个相当大的 UIKit 应用程序,并逐渐迁移到 SwiftUI。我希望逐步重新设计导航,一次一个区域的应用程序。

答:

0赞 lazarevzubov 11/20/2023 #1

正如 lorem ipsum 为我正确指出的那样,问题出在 中,这在 SwiftUI 视图之外不起作用。如果注入到 的根视图,则导航将按预期工作。@ObservedObjectNavigationModelUIHostingController

但是,它也迫使我们在视图内移动相应的视图修饰符。这打破了将视图与导航分离的想法,SwiftUI 视图变得“相互感知”。NavigationStacknavigationDestination

为了部分解决硬编码导航的问题,我们可以从外部注入目标块,如下所示:

struct FirstView<Destination: View>: View {

  var body: some View {
    NavigationStack(path: $navigationModel.navigationPath) {
      Button("Open next") {
         self.navigationModel.navigationPath.append(HomeViewController.NavigationModel.Path.second)
      }
        .navigationDestination(for: HomeViewController.NavigationModel.Path.self, destination: navigationDestination)
    }
  }
  @ViewBuilder private let navigationDestination: (HomeViewController.NavigationModel.Path) -> Destination
  @ObservedObject private var navigationModel: HomeViewController.NavigationModel

  init(navigationModel: HomeViewController.NavigationModel, @ViewBuilder navigationDestination: @escaping (HomeViewController.NavigationModel.Path) -> Destination) {
    self.navigationModel = navigationModel
    self.navigationDestination = navigationDestination
  }

}

当然,这不是一个完美的解决方案,但它解决了视图相互“感知”的问题。

这是与以下内容相关的更新代码:UIHostingController

@objc func presentSecondViewController() {
  let view = FirstView(navigationModel: navigationModel) {
    switch $0 {
      case .second:
        SecondView()
    }
  }

  let vc = UIHostingController(rootView: view)
  present(vc, animated: true)
}