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

y0u0uwnf  于 12个月前  发布在  Swift
关注(0)|答案(2)|浏览(155)

我有一个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"
  }
}

字符串
故事板文件基本上是初始项目,除了我将VC嵌入到了一个nav vc下。


的数据
在上面的代码中,ListView是使用已弃用的NavigationView实现的,它与UIHostingController配合良好。ListViewWithNewAPI是使用新的NavigationStack API的新实现,我无法复制原始行为。
这里有一个比较两种行为的视频。请使用示例代码并进行演示,看看我们是否可以使用新的API实现原始行为。


7gyucuyw

7gyucuyw1#

NavigationStack取代了NavigationView。在你的代码中,你将UIHostingController压入到navigationController上。然而,ListViewWithNewAPI中的视图包含了一个NavigationStack。这意味着你现在有两个级别的“导航堆栈”,导致了不期望的行为。
以下是解决此问题的三个不同选项:
1.一种选择是在故事板中删除Navigation Controller。您不能在故事板中直接使用UIHostingController,因为它使用泛型,但您可以将其 Package 在(非UINavigationController)UIViewController中。或者您可以完全删除故事板并采用SwiftUI应用生命周期。然后您可以按原样使用ListViewWithNewAPI中的代码。
1.您可以继续使用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)
    }
}

字符串
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)
    }
}


将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)
}


这就是你如何使用导航模型

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)")
        }
    }
}

wvyml7n5

wvyml7n52#

我还遇到了这个双导航栏问题(UIKit UINavigationController推送一个UIHostingController,其rootView嵌入了NavigationStack)。对于我的用例,我真的想保持编程.navigationDestination(<type>) {…}NavigationStack导航,主要是因为我需要保持目标视图“懒惰”(所以旧的NavigationLink(<destination>, <label>)方法不会“懒惰”,除非我进行了重大的黑客攻击)。
我还需要为应用程序的其他非SwiftUI部分保留UIKit navigationController。所以我的解决方案是在SwiftUI视图中隐藏UIKit导航栏,然后在NavigationStack中模拟返回按钮。动画等不是无缝的,但它符合我的需求。

@available(iOS 16.0, *)
struct MyView: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        NavigationStack {
            VStack {
                Text("Blah blah blah...")
            }
            // SwiftUI navigation bar properties
            .navigationTitle("My View")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                // Emulate a back button to take us back to the UIKit controller
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        dismiss()
                    } label: {
                        Image(systemName: "chevron.left")
                    }

                }
            }
        }
        .toolbar(.hidden, for: .navigationBar) // Hide UIKit navigation bar
    }
}

字符串
我还需要一个自定义的UIHostingController,以便在从SwiftUI视图转换回UIKit视图时重新显示UIKit导航栏。

@available(iOS 16.0, *)
class MyViewHostingController: UIHostingController<MyView> {
    private var wasNavigationBarHidden: Bool?
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(rootView: MyView) {
        super.init(rootView: rootView)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        wasNavigationBarHidden = navigationController?.isNavigationBarHidden
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        // Re-display the navigation bar
        if let wasNavigationBarHidden,
            let navigationController,
           navigationController.isNavigationBarHidden != wasNavigationBarHidden
        {
            navigationController.isNavigationBarHidden = wasNavigationBarHidden
        }
    }
}

相关问题