SwiftUI - NavigationLink inside NavigationBarItems, returns error on navigating from detail back to master view

Here's the gist of the problem: I have a NavigationLink inside the .navigationBarItems(trailing: LinkHere {ImageHere }) property for a view. When I tap the link, it directs to the new view just fine. However, when I tapthe back button, the app throws the following error, along with a signal abort error on the AppDelegate class line:


2019-10-22 12:17:55.403091-0700 MyApp[5203:116164] *** Assertion failure in -[UINavigationController popToViewController:transition:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3900.12.2.1/UINavigationController.m:8129

2019-10-22 12:17:55.412311-0700 MyApp[5203:116164] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Tried to pop to a view controller that doesn't exist.'

*** First throw call stack:

(

0 CoreFoundation 0x00007fff23c0b02e __exceptionPreprocess + 350

1 libobjc.A.dylib 0x00007fff50b00b20 objc_exception_throw + 48

2 CoreFoundation 0x00007fff23c0ada8 +[NSException raise:format:arguments:] + 88

3 Foundation 0x00007fff25684b61 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191

4 UIKitCore 0x00007fff470ab08d __57-[UINavigationController popToViewController:transition:]_block_invoke + 620

5 UIKitCore 0x00007fff470aad3a -[UINavigationController popToViewController:transition:] + 753

6 SwiftUI 0x00007fff2c0cb7dd $s7SwiftUI21UIKitNavigationBridgeC3pop33_F345616596EA75D1F4200D7666E5E588LL8animatedySb_tF + 413

7 SwiftUI 0x00007fff2c0caaa3 $s7SwiftUI21UIKitNavigationBridgeC20preferencesDidChangeyyAA14PreferenceListVF + 1267

8 SwiftUI 0x00007fff2c05691d $s7SwiftUI14_UIHostingViewC20preferencesDidChangeyyF + 477

9 SwiftUI 0x00007fff2c1555dd $s7SwiftUI9ViewGraphC13updateOutputs2atyAA4TimeV_tF + 221

10 SwiftUI 0x00007fff2c4a8629 $s7SwiftUI16ViewRendererHostPAAE6render8interval17updateDisplayListySd_SbtFyyXEfU_yyXEfU_ + 1001

11 SwiftUI 0x00007fff2c4a803a $s7SwiftUI16ViewRendererHostPAAE6render8interval17updateDisplayListySd_SbtFyyXEfU_ + 634

12 SwiftUI 0x00007fff2c49c094 $s7SwiftUI16ViewRendererHostPAAE6render8interval17updateDisplayListySd_SbtF + 436

13 SwiftUI 0x00007fff2c637e42 $s7SwiftUI14_UIHostingViewC14layoutSubviewsyyF + 226

14 SwiftUI 0x00007fff2c637e65 $s7SwiftUI14_UIHostingViewC14layoutSubviewsyyFTo + 21

15 UIKitCore 0x00007fff47ca0dc5 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2478

16 QuartzCore 0x00007fff2b0f3db1 -[CALayer layoutSublayers] + 255

17 QuartzCore 0x00007fff2b0f9fa3 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 517

18 QuartzCore 0x00007fff2b1058da _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 80

19 QuartzCore 0x00007fff2b04c838 _ZN2CA7Context18commit_transactionEPNS_11TransactionEd + 324

20 QuartzCore 0x00007fff2b081b41 _ZN2CA11Transaction6commitEv + 643

21 QuartzCore 0x00007fff2b0824aa _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 76

22 CoreFoundation 0x00007fff23b6d617 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23

23 CoreFoundation 0x00007fff23b680ae __CFRunLoopDoObservers + 430

24 CoreFoundation 0x00007fff23b6872a __CFRunLoopRun + 1514

25 CoreFoundation 0x00007fff23b67e16 CFRunLoopRunSpecific + 438

26 GraphicsServices 0x00007fff383d1bb0 GSEventRunModal + 65

27 UIKitCore 0x00007fff477bcef8 UIApplicationMain + 1621

28 MyApp 0x000000010fd07e0b main + 75

29 libdyld.dylib 0x00007fff51986c39 start + 1

)

libc++abi.dylib: terminating with uncaught exception of type NSException

(lldb)


The following StackOverflow link has the same problem for me, and the code is, in essence, the same:
https://stackoverflow.com/questions/58404725/why-does-my-swiftui-app-crash-when-navigating-backwards-after-placing-a-navigat

Has there been any solution for this? Is it really just a bug?

Post not yet marked as solved Up vote post of TroyRO Down vote post of TroyRO
15k views

Answers

very similar issue, going back is ok, but when I press the nav bar button again, this pops up..

similar issue. I have a "normal" NavigationLink for my details view and going back to the list view causes the same crash as mentioned.

All used to work well with ios 13.1 (iPhone and iPad), after upgrading to ios 13.2, the crash occurs.

same issue over here guys.I have also a normal NavigationLink which crashed after going back.

Hope someone can help.

Hope Apple will fix this soon.

Same problem here. After deactivating most of my tabs, and just have a list in one of them (the two others contains a Text("Hello")).

Any back action after pushing any new view, triggers a crash.


This does not occur when the list view is not in a TabView.

Could you show the code, just in case…

This code makes it go bang for me, based on TroyRO's original problem:


struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
                .navigationBarItems(trailing: NavigationLink(destination: DetailView()) {
                    Text("Press Me")
                })
        }
    }
}

struct MasterView: View {
    var body: some View {
        Text("Master View")
    }
}

struct DetailView: View {
    var body: some View {
        Text("Detail View")
    }
}


Interestingly, the view does appear to come back correctly, only crashing once the animation is done. Whether this is down to _UITransitionView using snapshots rather than live views, though, I'm not sure. I'm going to go some investigation of the view hierarchy for this and a List-based example to see if there's a difference depending on the location of the navigation link.

Okay, some spelunking here. Be prepared for a brain dump.


I put a List into the MasterView above, so you can jump to the detail view from five rows of the list or from the button in the nav bar.


Going from the row, the resulting hierarchy appears to contain two UINavigationControllers:


UIHostingController
⎣ _UIHostingView
  ⎣ PlatformViewHost<platformviewcontrollerrepresentableadaptor<bridgedsplitview<element, group<_conditionalcontent<element,="" _unaryviewadaptor<emptyview="">>>>>>
    ⎣ NotificationSendingSplitViewController 
      ⎣ _UISplitViewControllerPanelImplView
        ⎣ _UIPanelControllerContentView
          ⎣ UINavigationController
            ⎣ UILayoutContainerView
              ⎣ UINavigationTransitionView
                ⎣ UIViewControllerWrapperView
                  ⎣ UINavigationController
                    ⎣ UILayoutContainerView
                      ⎣ UINavigationTransitionView
                        ⎣ UIViewControllerWrapperView
                          ⎣ DestinationHostingController
                            ⎣ _UIHostingView


You can see them on lines 7 and 11, both with the usual UILayoutContainerView, UINavigationTransitionView, and UIViewControllerWrapperView within them. Skipping those internal items, there's a UINavigationController containing a UINavigationController. That seems weird to start with.


Here's the content of the outermost navigation controller, on line 7:


(lldb) po $17.viewControllers
<__NSArrayI 0x600000e35960>(
  <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8a11909470>,
  
)


Hm, yep, there's a nav controller containing a nav controller. The ordering there is from the bottom to the top of the navigation stack, so MasterView is held by that `UIHostingController<_VariadicView_Children.Element>`, and the pushed content is inside the secondary UINavigationController. That's borne out when looking at its content:


(lldb) po $19.viewControllers
<__NSSingleObjectArrayI 0x600000d59190>(
  <_TtGC7SwiftUIP13$7fff2c6519c028DestinationHostingControllerVS_7AnyView_: 0x7f8a11915bd0>
)


When we pop back to the master view, the secondary navigation controller from line 11 is gone, replaced with a UIHostingController:


UIHostingController
⎣ _UIHostingView
  ⎣ PlatformViewHost<platformviewcontrollerrepresentableadaptor<bridgedsplitview<element, group<_conditionalcontent<element,="" _unaryviewadaptor<emptyview="">>>>>>
    ⎣ NotificationSendingSplitViewController
      ⎣ _UISplitViewControllerPanelImplView
        ⎣ _UIPanelControllerContentView
          ⎣ UINavigationController
            ⎣ UILayoutContainerView
              ⎣ UINavigationTransitionView
                ⎣ UIViewControllerWrapperView
                  ⎣ UIHostingController
                    ⎣ _UIHostingView

Sure enough, there's now only the one controller within the root navigation controller now:


(lldb) po $17.viewControllers
<__NSSingleObjectArrayI 0x600000d454e0>(
  <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8a11909470>
)


Now let's hit the button in the navigation bar and see what happens to that list:


(lldb) po $17.viewControllers
<__NSArrayI 0x600000e36520>(
  <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7f8a11909470>,
  <_TtGC7SwiftUIP13$7fff2c6519c028DestinationHostingControllerVS_7AnyView_: 0x7f8a0f41f490>
)


Now that's what I'd expect to see normally—the new view is added to the list of the root nav controller, without any secondary nav controllers turning up. However, that's a quite major difference right there. Here's the hierarchy from that first nav controller now:


UINavigationController
⎣ UILayoutContainerView
  ⎣ UINavigationTransitionView
    ⎣ UIViewControllerWrapperView
      ⎣ DestinationHostingController
        ⎣ _UIHostingView


Oh, and look at this: that secondary navigation controller is still around somewhere, because we can query its content:


(lldb) po $19.viewControllers
<__NSSingleObjectArrayI 0x600000d5dd80>(
  <_TtGC7SwiftUIP13$7fff2c6519c028DestinationHostingControllerVS_7AnyView_: 0x7f8a11915bd0>
)


That there is the same content it had before, in fact.


Now let's pop back to the root view and take a look at things when it throws the exception (we'll enable breakpoints for Swift errors and all ObjC/C++ exceptions). Note that the addresses change, because I tried to get a view dump and it didn't like that, so I had to run the test again.


Here are the contents of the outer ($16) and inner ($18) view controllers:


(lldb) po $16


(lldb) po $16.viewControllers
<__NSSingleObjectArrayI 0x600002501cc0>(
  <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7fdebc700b50>
)

(lldb) po $18


(lldb) po $18.viewControllers
<__NSSingleObjectArrayI 0x600002571cc0>(
  <_TtGC7SwiftUIP13$7fff2c6519c028DestinationHostingControllerVS_7AnyView_: 0x7fdebc61cbd0>
)


Neither of those can pop at this point: they both contain only a single item. Which one are we looking at, though? Let's look at the stack. The caller of `-popToViewController:transition:` is `UIKitNavigationBridge.pop(animated:)`. Selecting that in the debugger lets us look at the state of its registers at that time, and r12–r15 are set (those same registers' contents are saved in the target function). Three of those registers contain 0x7fdebe810e00, which is the address of the outermost view controller. It thus appears that one was asked to pop one extra time.


More to follow…

Hokay, let's run it again with a breakpoint on -[UINavigationController popToViewController:animated:]. We push via a list row, then pop. Interestingly, the entire animation completes before -popToViewController:animated: is invoked, as though SwiftUI is actually doing that whole thing itself—which is interesting.


However, when it's done we see that method being invoked on a UINavigationController at 0x7fbd13035200, and it's being asked to pop to an instance of UIHostingController<_VariadicView_Children.Element> at 0x7fbd1782e940. The navigation controller has this to say about its content:


(lldb) po [$rdi viewControllers]
<__NSSingleObjectArrayI 0x60000139c6b0>(
  <_TtGC7SwiftUI19UIHostingControllerVVS_22_VariadicView_Children7Element_: 0x7fbd1782e940>
)

(lldb)


In other words, it only contains that one item—the same one to which it's being asked to pop. So it seems that the error is less likely about emptying the stack, but about asking to go back to a view that isn't in the stack. Going into and out of another list row shows the same behavior, with the same views at the same addresses, with the same result.


Let's see what happens when we go via the nav bar button:


(lldb) po $rax
<UINavigationController: 0x7fbd13035200>

(lldb) po $rdi
<UINavigationController: 0x7fbd13035200>

(lldb)


So, it's asking the navigation view to pop to itself. Well, it's not its own content, so naturally that fails—causing the crash.


Now to figure out why. Some initial spelunking into the code for UIKitNavigationBridge.pop(animated:) shows a call to parentViewController shortly before the call to -popToViewController:animated:. The parentViewController of the UIHostingController is—surprising no-one—the navigation controller. So I wonder if it's not expecting to be looking at some special sub-controller that's not being pushed onto the stack. More spelunking on the non-crashing path should lead in the right direction, though I'm also going to feed SwiftUI into a better disassembler to see if I can't trace the logic a little more.

It looks like instances of UIKitNavigationBridge contain a couple of properties used when popping: containingNavController and containingVC. It uses these to check some stuff, more or less like this:


if let navController = self.containingNavController,
   let vc = self.containingVC,
   let parent = vc.parentViewController
{
    let target: UIViewController
    if (!(parent is UINavigationController)) || (parent == navController) {
        target = vc
    }
    else {
        target = parent
    }

    navController.popToViewController(target, animated: true)
}


So, it has a nav controller property and a view controller property. In the happy path, containingVC.parentViewController == containingNavController, so it uses containingVC as the target (line 7). In the unhappy path, containingVC.parentViewController is not a UINavigationView, so it again uses containingVC as the target. However, in the unhappy path, both properties refer to the same navigation controller. Thus, I think it's the push code that's actually doing something wrong.


Aside: from what I recall of the early days of SwiftUI (and it's changed a LOT since then), the NavigationView nesting was by design (albeit using NavigationView, not UINavigationController). Back then it operated something like a linked list of view wrappers, with each node containing something it could use to be dismissed except for the root.

same issue here. very inspiring software quality. i guess no one at apple is actually using swiftui ...

Current workaround that works for me as it seems to be a problem with iOS13.2, deploy it to simulator running iOS 13.1.

Hello, the problem is simulator, if you to run your device it work, but your only device version IOS 13.1, is version IOS 13.2 don't work.

unfortunately there is no 13.1 simulatior 😟

Great! Better than nothing... 😉

Okay. This was an amazing debug excursion. Seriously, it helps a great deal to provide understanding of what's happening under the hood.


But despite all this, how do we work around this? Please!