为什么在将 `NavigationLink` 放入 `NavigationView` 中的 `navigationBarItems` 后向后导航时,我的 SwiftUI 应用程序会崩溃?

Why does my SwiftUI app crash when navigating backwards after placing a `NavigationLink` inside of a `navigationBarItems` in a `NavigationView`?

编辑:这已在 iOS 13.3!

中修复

最小可重现示例(Xcode 11.2 beta,这适用于 Xcode 11.1):

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
            .navigationBarItems(
                leading: Button(
                    action: {
                        self.presentation.wrappedValue.dismiss()
                    },
                    label: { Text("Back") }
                )
            )
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}

问题似乎在于将我的 NavigationLink 放在 navigationBarItems 修饰符中,该修饰符嵌套在根视图为 NavigationView 的 SwiftUI 视图中。崩溃报告表明,当我向前导航到 Child 然后返回到 Parent.

时,我试图弹出一个不存在的视图控制器
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
*** First throw call stack:

如果我像下面那样将 NavigationLink 放在视图的主体中,它工作得很好。

struct Parent: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: Child(), label: { Text("Next") })
        }
    }
}

这是 SwiftUI 错误还是预期行为?

编辑:我已经在他们的反馈助手中用 ID FB7423964 向 Apple 提出了一个问题,以防 Apple 的任何人关心权衡:)。

编辑:我在反馈助手中打开的票表明有 10 多个类似的报告问题。他们已将决议更新为 Resolution: Potential fix identified - For a future OS update。祈祷修复程序很快就会落地。

这对我来说是一个痛点!我离开了它,直到我的大部分应用程序完成并且我有想法 space 来处理崩溃。

我想我们都同意 SwifUI 有一些非常棒的东西,但调试可能很困难。

在我看来,我会说这是一个BUG。这是我的理由:

  • 如果将 presentationMode dismiss 调用包装在大约半秒的异步延迟中,您应该会发现程序将不再崩溃。

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.presentationMode.wrappedValue.dismiss()
    } 
    
  • 这向我表明,该错误是一种意想不到的行为,深入到 SwiftUI 如何与所有其他 UIKit 代码交互以管理各种视图。根据您的实际代码,您可能会发现,如果视图中存在一些较小的复杂性,则实际上不会发生崩溃。例如,如果您从一个视图退出到一个有列表的视图,而该列表是空的,您将在没有异步延迟的情况下发生崩溃。另一方面,如果您在该列表视图中只有一个条目,强制循环迭代生成父视图,您将看到不会发生崩溃。

我不太确定我的延迟包装解雇调用的解决方案有多稳健。我必须对其进行更多测试。如果您对此有任何想法,请告诉我!我很乐意向你学习!

虽然我没有看到任何崩溃,但您的代码有一些问题:

通过设置前导项,您实际上取消了导航转换的默认行为。 (尝试从前侧滑动以查看是否有效)。

所以那里不需要按钮。保持原样,您将有一个免费的后退按钮。

并且不要忘记根据 HIG,后退按钮标题应该显示它的位置,而不是它是什么!因此,请尝试为第一页设置一个标题,以在弹出的任何后退按钮上显示它。

struct Parent: View {
    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarItems(
                    trailing: NavigationLink(destination: Child(), label: { Text("Next") })
                )
                .navigationBarTitle("First Page",displayMode: .inline)
        }
    }
}

struct Child: View {
    @Environment(\.presentationMode) var presentation
    var body: some View {
        Text("Hello, World!")
    }
}

struct ContentView: View {
    var body: some View {
        Parent()
    }
}

这是一个主要错误,我看不到解决它的正确方法。在 iOS 13/13.1 中运行良好,但 13.2 崩溃。

您实际上可以用更简单的方式复制它(这段代码几乎就是您所需要的)。

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Hello, World!").navigationBarTitle("To Do App")
                .navigationBarItems(leading: NavigationLink(destination: Text("Hi")) {
                    Text("Nav")
                    }
            )
        }
    }
}

希望 Apple 解决这个问题,因为它肯定会破坏大量 SwiftUI 应用程序(包括我的)。

根据你们提供的信息,特别是@Robert 就 NavigationView 的放置位置所做的评论,我找到了一种方法来解决这个问题,至少在我的特定情况下是这样。

在我的例子中,我有一个包含在 NavigationView 中的 TabView,如下所示:

struct ContentViewThatCrashes: View {
@State private var selection = 0

var body: some View {
    NavigationView{
        TabView(selection: $selection){
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("first")
                    Text("First")
                }
            }
            .tag(0)
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
            .tabItem {
                VStack {
                    Image("second")
                    Text("Second")
                }
            }
            .tag(1)
        }
    }
  }
}

此代码崩溃,因为每个人都在 iOS 13.2 中报告并在 iOS 13.1 中工作。经过一些研究,我找到了解决这种情况的方法。

基本上,我将 NavigationView 分别移动到每个选项卡上的每个屏幕,如下所示:

struct ContentViewThatWorks: View {
@State private var selection = 0

var body: some View {
    TabView(selection: $selection){
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("First View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("first")
                Text("First")
            }
        }
        .tag(0)
        NavigationView{
            NavigationLink(destination: NewView()){
                Text("Second View")
                    .font(.title)
            }
        }
        .tabItem {
            VStack {
                Image("second")
                Text("Second")
            }
        }
        .tag(1)
    }
  }
}

在某种程度上违背了 SwiftUI 的简单性前提,但它适用于 iOS 13.2。

这也让我很沮丧。几个月以来,根据Xcode版本、模拟器版本和真实设备类型and/or版本,从能用到不能再用,貌似随机。但是,最近它对我来说一直在失败,所以昨天我深入研究了它。我目前正在使用 Xcode 版本 11.2.1 (11B500)。

问题似乎与导航栏以及向其添加按钮的方式有关。因此,我没有对按钮本身使用 NavigationLink(),而是尝试使用标准 Button() 和一个设置 @State 变量的操作,该变量可激活隐藏的 NavigationLink。这是 Robert 的父视图的替代品:

struct Parent: View {
    @State private var showingChildView = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
                NavigationLink(destination: Child(),
                               isActive: self.$showingChildView)
                { EmptyView() }
                    .frame(width: 0, height: 0)
                    .disabled(true)
                    .hidden()            
             }
             .navigationBarItems(
                 trailing: Button(action:{ self.showingChildView = true }) { Text("Next") }
             )
        }
    }
}

对我来说,这在所有模拟器和所有真实设备上都非常一致。

这是我的助手意见:

struct HiddenNavigationLink<Destination : View>: View {

    public var destination:  Destination
    public var isActive: Binding<Bool>

    var body: some View {

        NavigationLink(destination: self.destination, isActive: self.isActive)
        { EmptyView() }
            .frame(width: 0, height: 0)
            .disabled(true)
            .hidden()
    }
}

struct ActivateButton<Label> : View where Label : View {

    public var activates: Binding<Bool>
    public var label: Label

    public init(activates: Binding<Bool>, @ViewBuilder label: () -> Label) {
        self.activates = activates
        self.label = label()
    }

    var body: some View {
        Button(action: { self.activates.wrappedValue = true }, label: { self.label } )
    }
}

这是一个用法示例:

struct ContentView: View {
    @State private var showingAddView: Bool = false
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello, World!")
                HiddenNavigationLink(destination: AddView(), isActive: self.$showingAddView)
            }
            .navigationBarItems(trailing:
                HStack {
                    ActivateButton(activates: self.$showingAddView) { Image(uiImage: UIImage(systemName: "plus")!) }
                    EditButton()
            } )
        }
    }
}

作为解决方法,根据 Chuck H 的上述回答,我将 NavigationLink 封装为隐藏元素:

struct HiddenNavigationLink<Content: View>: View {
var destination: Content
@Binding var activateLink: Bool

var body: some View {
    NavigationLink(destination: destination, isActive: self.$activateLink) {
        EmptyView()
    }
    .frame(width: 0, height: 0)
    .disabled(true)
    .hidden()
}
}

然后您可以在 NavigationView 中使用它(这很重要)并从导航栏中的 Button 触发它:

VStack {
    HiddenNavigationList(destination: SearchView(), activateLink: self.$searchActivated)
    ...
}
.navigationBarItems(trailing: 
    Button("Search") { self.searchActivated = true }
)

将其包裹在“//HACK”注释中,以便在 Apple 修复此问题时您可以替换它。

FWIW - 上述建议隐藏 NavigationLink Hack 的解决方案仍然是 iOS 13.3b3 中的最佳解决方法。为了后代的缘故,我还提交了 FB7386339,并且与上述其他 FB 类似地被关闭:"Potential fix identified - For a future OS update"。

双手合十。

在iOS13.3中解决。只需更新您的 OS 和 xCode.

Xcode 11.2.1 Swift 5

知道了!我花了几天时间才弄明白这个...

在我的例子中,当我使用 SwiftUI 时,只有当我的列表底部超出屏幕然后我尝试 "move" 任何列表项时,我才会崩溃。我最终发现,如果我在 List() 下面有太多 "stuff",那么它会在移动时崩溃。例如,在我的 List() 下面有一个 Text()、Spacer()、Button()、Spacer() Button()。如果我注释掉这些对象中的任何一个,那么我突然无法重现崩溃。我不确定限制是什么,但如果您遇到此崩溃,请尝试删除列表下方的对象以查看是否有帮助。