I've been trying to understand for the past several weeks why my app has been crashing at times when I save my app's NavigationPath.CodableRepresentation
to core data (as a Binary Data type), terminate the app, and then have the app automatically re-initialize and set my old path after opening the app again.
Occasionally – when I'd navigated a couple views deep on the previous session – after the old path was restored, the console would throw a couple errors such as:
Failed to decode item in navigation path at index 1. Perhaps the navigationDestination declarations have changed since the path was encoded?
...and:
Missing navigation destination while decoding a NavigationPath. No navigationDestination(for: <Hashable>.self) { … } was found among the views on the path.
...and then the app would crash with the following messages:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '<SwiftUI.UIKitNavigationController: 0x103031200> is pushing the same view controller
instance (<_TtGC7SwiftUI32NavigationStackHostingControllerVS_7AnyView_: 0x102853400>) more than
once which is not supported and is most likely an error in the application : <Application Name>'
*** First throw call stack:
(0x1b616a69c 0x1ae41fc80 (...and so on))
libc++abi: terminating due to uncaught exception of type NSException
...and I FINALLY was able to isolate the cause today in a test project.
It turns out that nesting a subview inside of a List
makes all navigation destinations inside that subview invisible to the parent view. For example, if you were to have the following setup:
struct ContentView: View {
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
NavigationLink(value: "I'm a subview!") {
Text("Go deeper...")
}
.navigationDestination(for: String.self) { string in
SubView(string: string)
}
Button("Go as deep as we can go!") {
navigationPath.append("You went right past me!")
navigationPath.append(NSString(string: "Or at leat, I was..."))
}
}
}
}
struct SubView: View {
var string: String
var body: some View {
VStack {
Text(string)
NavigationLink(value: 1) {
Text("And deeper...")
}
.navigationDestination(for: NSString.self) { nsString in
DeepSubView(nsString: nsString)
}
}
}
}
struct DeepSubView: View {
var nsString: NSString
var body: some View {
VStack {
Text(String(nsString))
}
}
}
...you'd find that everything would work just fine — until you change the VStack
in SubView
to List
and try tapping on the button in ContentView
. You'll find that two views get pushed onto the stack, but the second view is a blank screen with a small '⚠️' icon in the middle of it.
Not only that, but if you coded the button append a second NSString (like so):
Button("Go as deep as we can go!") {
navigationPath.append("You went right past me!")
navigationPath.append(NSString(string: "Or at leat, I was..."))
navigationPath.append(NSString(string: "I'm the deepest you can go in this app!"))
}
...your console will throw the error:
A NavigationLink is presenting a value of type “NSString” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Note: Links search for destinations in any surrounding NavigationStack, then within the same column of a NavigationSplitView.
(Note: Even if you were to change the List
in SubView
back to a VStack
, and then wrap the declaration of SubView
in the navigationDestination
block in a List
:
.navigationDestination(for: String.self) { string in
List {
SubView(string: string)
}
}
...you'll find that you have the same problem.)
And so, if you automatically attempt to load a saved path when opening your app, and one of the navigation destinations for a value in your saved path either:
Lives inside a
List
in one of your sub-views, ORLives inside a subview that's wrapped in a
List
...then your app will crash.
(Another Note: If you wrap navigation destinations on your app's initial view in a List
, then they too will be invisible to the app when it first loads. However, if your loaded path only contains values for those navigation destinations in your initial view, then your app won't crash or throw any errors — it will just stay on the initial view.)
This one has been quite the head-banger for me, so I hope posting this will help someone else out, too! If you have any guesses as to why this behavior is (or even how to make these list-embedded navigation destinations work ), please leave a reply!
(My guess is that it has something to do with why you can't have a List
inside of another List
within the same view, but who knows ♂️?)