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

IosSwiftUinavigationcontrollerSwiftuixcode11.2

Ios Problem Overview


EDIT: This has been fixed in iOS 13.3!

Minimal reproducible example (Xcode 11.2 beta, this works in 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()
    }
}

The issue seems to lie in placing my NavigationLink inside of a navigationBarItems modifier that's nested inside of a SwiftUI view whose root view is a NavigationView. The crash report indicates that I'm trying to pop to a view controller that doesn't exist when I navigate forward to Child and then back to Parent.

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'
*** First throw call stack:

If I were to instead place that NavigationLink in the body of the view like the below, it works just fine.

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

Is this a SwiftUI bug or expected behavior?

EDIT: I've opened an issue with Apple in their feedback assistant with the ID FB7423964 in case anyone out there from Apple cares to weigh in :).

EDIT: My open ticket in the feedback assistant indicates there are 10+ similar reported issues. They've updated the resolution with Resolution: Potential fix identified - For a future OS update. Fingers crossed that the fix lands soon.

Ios Solutions


Solution 1 - Ios

This was quite a pain point for me! I left it until most of my app was completed and I had the mind space to deal with the crashing.

I think we can all agree that there's some pretty awesome stuff with SwifUI but that the debugging can be difficult.

In my opinion, I would say that this is a BUG. Here is my rationale:

  • If you wrap the presentationMode dismiss call in an asynchronous delay of about a half-second, you should find that the program will no longer crash.

     DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
         self.presentationMode.wrappedValue.dismiss()
     } 
    
  • This suggests to me that the bug is an unexpected behaviour way down deep in how SwiftUI interfaces with all the other UIKit code to manage the various views. Depending on your actual code, you might find that if there is some minor complexity in the view, the crash actually will not happen. For example, if you are dismissing from a view to one that has a list, and that list is empty, you will get a crash without the asynchronous delay. On the other hand, if you have even just one entry in that list view, forcing a loop iteration to generate the parent view, you'll see that the crash will not occur.

I'm not so sure how robust my solution of wrapping the dismiss call in a delay is. I have to test it much more. If you have ideas on this, please let me know! I'd be very happy to learn from you!

Solution 2 - Ios

This also has frustrated me for quite some time. Over the past few months, depending on the Xcode version, simulator version and real device type and/or version, it has gone from working to failing to working again, seemingly at random. However, recently it has been failing consistently for me, so yesterday I took a deep dive into it. I am currently using Xcode Version 11.2.1 (11B500).

It looks like the issue revolves around the Nav Bar and the way the buttons were added to it. So instead of using a NavigationLink() for the button itself, I tried using a standard Button() with an action that sets a @State var that activates a hidden NavigationLink. Here is a replacement for Robert's Parent View:

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

For me, this works very consistently across all simulators and all real devices.

Here are my helper views:

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

Here is an example of the usage:

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

Solution 3 - Ios

This is a major bug and I can't see a proper way to work around it. Worked fine in iOS 13/13.1 but 13.2 crashes.

You can actually replicate it in a much simpler way (this code is literally all you need).

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

Hope Apple sort it out as it will surely break loads of SwiftUI apps (including mine).

Solution 4 - Ios

As a workaround, based on Chuck H's answer above, I've encapsulated the NavigationLink as a hidden element:

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

Then you can use it within a NavigationView (which is crucial) and trigger it from a Button in a nav bar:

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

Wrap this in "//HACK" comments so when Apple fixes this you can replace it.

Solution 5 - Ios

Based on the information that you guys provided and specially a comment that @Robert made about where the NavigationView is placed I have found a way to workaround the issue at least on my specific scenario.

In my case I had a TabView that was enclosed in a NavigationView like this:

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

This code crashes as everyone is reporting in iOS 13.2 and works in iOS 13.1. After some research I figured out a workaround to this situation.

Basically, I am moving the NavigationView to each screen separately on each tab like this:

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

Somehow goes against the SwiftUI premise of simplicity but it works on iOS 13.2.

Solution 6 - Ios

Xcode 11.2.1 Swift 5

GOT IT! It took me a couple days to figure this one out...

In my case when using SwiftUI I am getting a crash only if the bottom of my list extended beyond the screen and then I try to "move" any list items. What I ended up finding out is that if I have too much "stuff" underneath the List() then it crashes on the move. For instance, below my List() I had a Text(), Spacer(), Button(), Spacer() Button(). If I commented out any ONE of those objects then suddenly I could not recreate the crash. I am not certain what the limitations are, but if you are getting this crash then try removing objects below your list to see if it helps.

Solution 7 - Ios

Although I can't see any crashes, your code has some issues:

by setting the leading item, you actually kill the default behavior of the navigation transitions. (try swipe from leading side to see if it works).

So no need to have a button there. Just leave it as it is and you have a free back button.

And don't forget according to HIG, back button title should show where it goes, not what it is! So try to set a title for the first page to show it one the any back button that pops to it.

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

Solution 8 - Ios

FWIW - The solutions above suggesting a hidden NavigationLink Hack is still the best workaround in iOS 13.3b3. I have also filed a FB7386339 for posterity's sake, and was closed similarly to other aforementioned FBs: "Potential fix identified - For a future OS update".

Fingers Crossed.

Solution 9 - Ios

It is solved in iOS 13.3. Just update your OS and xCode.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobertView Question on Stackoverflow
Solution 1 - IosJustin NganView Answer on Stackoverflow
Solution 2 - IosChuck HView Answer on Stackoverflow
Solution 3 - IosJamesView Answer on Stackoverflow
Solution 4 - IosP. EntView Answer on Stackoverflow
Solution 5 - IosJulio BailonView Answer on Stackoverflow
Solution 6 - IosDave LevyView Answer on Stackoverflow
Solution 7 - IosMojtaba HosseiniView Answer on Stackoverflow
Solution 8 - IosMike W.View Answer on Stackoverflow
Solution 9 - IosFRIDDAYView Answer on Stackoverflow