Why Does My Swiftui App Crash When Navigating Backwards After Placing a 'Navigationlink' Inside of a 'Navigationbaritems' in a 'Navigationview'

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

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!

SwiftUI navigation bar inline mode searchable issue

I found a simple workaround, download Xcode 13.2.1. No bugs present when running it on iOS 15.4+. Compiling from an Xcode version before 13.3 seems to do the trick. I assumed I needed the latest version of Xcode for App Store submissions.

NavigationLink Works Only for Once

[UPDATE] Nov 5, 2020 - pawello2222 says that this issue has been fixed in Xcode 12.1.


[UPDATE] Jun 14, 2020 - Quang Hà says that this issue has come back in Xcode 11.5.


[UPDATE] Feb 12, 2020 - I checked for this issue in Xcode 11.4 beta and found that this issue has been resolved.


I was getting the same issue in my project too, when I was testing it in Xcode's simulator. However, when I launched the app on a real device (iPhone X with iOS 13.3), NavigationLink was working totally fine. So, it really does seem like Xcode's bug.

Why does SwiftUI recall an existing view's body which contains stale data?

First, to answer my original question. The behavior is due to the way how ObservableObject (or EnvironmentObject) invalidates view. Both ListView and DetailView monitor data model changes by subscribing ObservableObject's objectWillChange publisher. Since it's impossible to control which subscriber receives change first, which view's body gets called is undefined. In practice, it's DetailView's body gets called first, that's the reason why the regular property in the detail view doesn't get updated.

Note that, while in this case we can give a concrete explanation and modify our code accordingly, in general it's unreliable to write code based on our understanding when a view's body gets called. SwiftUI may call a view's body at unexpected time. See this example. I believe when and how a view's body get called should be considered as SwiftUI implementation details. In other words, it's undefined.

Why is this an issue? Well, it's because when we call data model API in body's code (either view rendering code or intent code) we need to pass view specific data (e.g., regular properties of the view in the example code) as param to the API. Note while these view specific data are retrieved from data model, once they are saved in the view, they can go stale at any time. Since I use force unwrapping, it causes crash.

Why do I use force unwrapping? It's because I underestimated SwiftUI's complexity. I thought how views were invalidated are simple and predictable. I also thought when data model changed, the entire view hiearchy would be recreated first and then views with change would get their body called (and hence view specific data and data model were always consitent). It turned out both are wrong.

So, how to address the view specific data and data model inconsistency issue?

Approach 1: Using binding.

This is Apple's approach. See here. This approach looks great. But unfortunately it doesn't seem feasible in most practical applications, for at least two reasons:

  1. It only suits for simple data models. For complex data models (e.g. it contains bank accounts and transers which refers to each by id) and deep view hierarchies (e.g. account A detail view -> transfer view -> account B detail view -> ...), it's impossible to prepare all the data ahead, as a result it has to call data model API. BTW, I did experiment to pass both view specific data and data model as binding, it seemed to work but I ran into the second issue.

  2. Binding doesn't work well when being passed to deep view hiearchy. In my experiment it caused weird view identity issue. I'd like to cite @Asperi's comment on this (he has a lot of great answers on SwiftUI and he made the comment in a unrelated question).

Binding works bad being transferred into deep view hierarchy, use instead ObservableObject based view model to inject for each next view layer, with some communication between view model to update only needed properties.

Approach 2: Using the usual view model approach.

This requires validating params. There are different ways to do it:

  • Move the view specific data to data model object and encapsulte the validation in data model.

  • Do it in view rendering code in body.

Either way the data model api needs to check invalid params (that is, it shouldn't do force unwrapping).

Note the code in this approach doesn't look as simple as Apple's approach. Also it may require the use of if statement. This confused me a lot because it doesn't fell like the SwiftUI way Apple advertised. But based on my current understanding, it has to be this way to write practicial applications.


Since the question plagued me a lot, I want to emphasize it's an architecture requirement for data model API to be lenient, because a) a view's body can be called at unexpected time, b) hence there is no way to make sure view specific data and data model are consistent, c) hence data model API have to handle invalid params if we'd like to call it in body.

The situation in Apple's approach is a bit different. But as explained above, it only works in simple cases.



Related Topics



Leave a reply



Submit