52

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.

de.
  • 7,068
  • 3
  • 40
  • 69
Robert
  • 981
  • 1
  • 15
  • 24
  • The example you provided above works just fine with Xcode 11.2 beta. Are we missing something here? – Subramanian Mariappan Oct 16 '19 at 05:22
  • @SubramanianMariappan It's working fine for me as well on 11.2 beta. – Farhan Amjad Oct 16 '19 at 08:31
  • 1
    Interesting, it crashes for me every time. I even tried creating a fresh project and copying that exact code in place of `ContentView.swift`. I'll make an edit to the post, but the crash only happens when you navigate forward and then back. – Robert Oct 16 '19 at 12:19
  • Great question! Your example here crashes for me every time too. I just posted a new answer that works very well for me. Let me know if it works for you as well. Thanks. – Chuck H Nov 16 '19 at 19:57
  • Confirm this crashes on Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2) – P. Ent Nov 20 '19 at 15:16
  • 1
    Thanks for the updates regarding the apple tickets! – malte Nov 21 '19 at 08:54

9 Answers9

30

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!

halfer
  • 19,824
  • 17
  • 99
  • 186
Justin Ngan
  • 1,050
  • 12
  • 20
  • 1
    Very clever! I hadn't thought of that. Hoping it gets fixed soon! – Robert Oct 20 '19 at 17:40
  • 1
    @Robert Did it fix your problem? This is a tough one since an unrelated issue that I have found is using a Picker inside child navigation views. While a segmented picker style works, the default appears to cause a crash at the same point, when clicking the back button. We can discuss further if it's still giving you grief. PS. I hate my solution. It's a hack but it's one that shouldn't require code update if Apple fixes the timing issue. – Justin Ngan Oct 21 '19 at 09:10
  • 2
    I agree that the timing aspect, along with the fact that it worked fine in 11.1 and works outside of `.navigationBarItems()` points to this being a bug. – John M. Oct 21 '19 at 17:17
  • 3
    Yes, I believe it is a bug and this is my current leading candidate for the bounty award. Since I have 4 days left on the bounty as of the time of this writing I'm just holding out in case anyone comes along with new info :). – Robert Oct 22 '19 at 15:11
  • 1
    This was a very interesting tip, thanks for that! Unfortunately I'm still reliably crashing the app in the simulator 100% of the time :/ It works better on device, but is not without crashing at all. But that was also the case without the delay. – Kilian Oct 25 '19 at 15:15
  • 1
    @Kilian Do you by chance have a picker in the child view? – Justin Ngan Oct 26 '19 at 17:15
  • 1
    @JustinNgan Not in this View, no. It's just a List in a navigation view. The parent view has an onAppear block, could that be of significance here? – Kilian Oct 26 '19 at 17:44
  • Absolutely a bug, can't believe Apple don't have some kind of automated UI regression testing for this, such a simple, basic use case. – James Nov 01 '19 at 23:09
  • surely the work around described here won't work if the user swipe from the left to go back. Maybe on manual dismiss? – James Nov 01 '19 at 23:10
  • That's a good point @James. It is a useful work around for the save and dismiss function but yes, it is isolated in applications. I've had to change the design of an app just because of this lack of a universal workaround. Many folks use on stack overflow have suggested using PresentationMode for dismissal but it is completely useless in cases where navigation views + forms + default pickers are used. I guess the only thing to remember is SwiftUI is in infancy though it is sort of painful and some of these issues seem basic. – Justin Ngan Nov 03 '19 at 14:09
  • still crashes with delay, only every time instead of sometimes.. – JerseyDevel Nov 05 '19 at 04:59
  • i also noticed, it crashes really bad, all the time on my small older iPad running 13.2, but not too bad on my iPhone 8 with 13.2 wonder if that could indicate something.. – JerseyDevel Nov 05 '19 at 05:11
  • I'm getting a delayed reaction, and buggy behaviour, rather than an outright crash. Not good. Here's hoping that 13.3 with a fix is not far away however it delays the release of a SwiftUI version of an upgrade app. I.e how long before the majority of users move to 13.3 or beyond. The clock is now reset. – Yarm Nov 16 '19 at 14:21
  • Seeing this answer for the first time, I had high hopes for it, but it did not work for me at all. I just posted a new answer that seems to work very reliably for me. – Chuck H Nov 16 '19 at 18:27
17

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()
            } )
        }
    }
}
Chuck H
  • 7,434
  • 4
  • 31
  • 34
  • I can confirm this works (really well for a hack ;-) )! Apple needs to fix this asap though. Xcode 11.2.1, Catalina 10.15.2 (beta), iOS 13.2.2 – P. Ent Nov 20 '19 at 17:17
  • 1
    I agree 100%. In general, with respect to navigation in SwiftUI, there is a lot that is either broken or just plain missing. Which of course leads us to the real problem. There is no "source of truth" (i.e. documentation and examples) from Apple, only hacks like us. BTW, I use the above technique so much, I have created two utility views that help a lot with readability. I will add them to my answer in case anyone is interested. – Chuck H Nov 20 '19 at 20:52
  • 1
    This doesn't work for me for more than one navigation. Once you've popped back to the previous screen, the invisible link no longer works. – Jon Shier Dec 13 '19 at 16:13
  • Looks like this is only broken on iOS 13.3. Workaround works on 13.2. Any new ideas? – Jon Shier Dec 13 '19 at 16:28
  • I have a minimal test case for my HiddenNavigationLink (based on the examples in this question) and it still works. The App I'm working on has over a dozen HiddenNavigationLinks and they all still work. My Xcode is at 11.3 (11C29) with testing done on a real iPhone X with iOS 13.3 (17C54). Xcode's deployment target is still 13.2 as there is no 13.3 available to select. If you have a minimal example that fails, please post it to github if you can. – Chuck H Dec 13 '19 at 19:18
  • Seeing same behavior as Jon in 13.3. Button doesn't do anything on the second tap after popping back. – Joey C. Dec 15 '19 at 07:45
  • 1
    I have several real devices at 13.3 (build 17C54) and they all work as desired. Since I do almost all of my testing on real devices, I don't use the simulator very often. But I did just try my test case on a 13.3 simulator and the test does fail there. I did notice that iOS 13.3 on the Xcode simulator is an earlier build (17C45) than the public update. I would be interested to know if anyone observes the failing behavior on a real device. – Chuck H Dec 15 '19 at 18:08
  • Thanks, you're right that on device this doesn't seem to be an issue. – Joey C. Dec 15 '19 at 21:40
  • Still broken in simulator in Xcode 11.3.1 (just out). – James Jan 15 '20 at 07:51
12

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

halfer
  • 19,824
  • 17
  • 99
  • 186
James
  • 1,985
  • 18
  • 19
  • Haha... That's pretty awesome. You've navigated to a Text view which in SwiftUI, is a view! Yeah, that should navigate back to it's parent shouldn't it? Yet, it doesn't. It's interesting that the behaviour from your example breaks the UI but doesn't actually cause a fatal crash. – Justin Ngan Nov 06 '19 at 10:30
  • Yeah the composability of SwiftUI (and React Native/Flutter etc) are incredible. Gives you so much control/flexibility (when it works at least). – James Nov 08 '19 at 16:06
  • 1
    Confirm this crashes on Catalina (10.15.1), Xcode (11.2.1), iOS (13.2.2) – P. Ent Nov 20 '19 at 15:16
  • It no longer crashes in 13.3, however the navigation only seems to work the first time you trigger it ‍♂️ – James Jan 11 '20 at 22:22
6

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
P. Ent
  • 1,654
  • 1
  • 12
  • 22
3

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
Julio Bailon
  • 3,735
  • 2
  • 33
  • 34
  • this works but, the problem is removing tabViews on the NewView. – FRIDDAY Nov 13 '19 at 08:09
  • 1
    @FRIDDAY this example works in 13.1 but crashes in 13.2. It is a known bug and my intention was to try to help someone in the same scenario with a workaround – Julio Bailon Nov 14 '19 at 14:36
  • I think that your problem might be that Swift (not SwiftUI) normally embeds NavigationControllers in TabViewControllers. I expect that SwiftUI would require a similar approach for using the two simultaneously. – Justin Ngan May 15 '20 at 01:40
  • Take a look at the bottombar package on github. You can look at the source code, its just 4 files. It is a good example of how you can achieve a desirable navigation hierarchy with a tab bar that hides on sub-views. I have verified implementing the pattern in this repo works for me on iOS 15.2 without crashes. https://github.com/smartvipere75/bottombar-swiftui – Andre Jan 18 '22 at 15:39
1

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.

Dave Levy
  • 1,036
  • 13
  • 20
0

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()
    }
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
0

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.

Mike W.
  • 69
  • 3
0

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

FRIDDAY
  • 3,781
  • 1
  • 29
  • 43
  • 1
    Xcode 11.3 (11C29) on 10.15.2 results in a different behaviour for me: The backwards navigation is working, but afterwards the NavigationLink has no function anymore. Clicking it does nothing. – malte Dec 22 '19 at 10:25
  • @malte It is better to open a new question for that. Before I check your code give your NavigationLink `.buttonStyle(PlainButtonStyle())` modifier and try it again. let me know if you asked a question. – FRIDDAY Dec 23 '19 at 08:50
  • 1
    You're right. Turns out there is already a new question: https://stackoverflow.com/questions/59279176/navigationlink-works-only-for-once – malte Dec 28 '19 at 11:22