NavigationStack $path cleared on dealloc?

Hello, This code has a NavigationSplitView, whose sidebar is a List and its detail contains a NavigationStack. It is controlled by two @Observable properties, a selection and a path. The path shown in the detail depends upon the selection in the List. If I programmatically change the selection and path, the path will be set, but via SwiftUI backtraces (pasted below), it will clear out my path. This makes my code lose state. What am I doing wrong? Is this a bug? I'm using Xcode16 running against the iOS 18 simulator (although this also happens with iOS17).

To reproduce, Launch the App, Note that you are on the "first" selection. Tap "Nav Path: Path: second-100". You'll go to the "second" selection, but the path will be empty. If you place a breakpoint when the $path is cleared, you'll see it is being cleared by SwiftUI.

Backtrace when the path is emptied:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x00000001043295c0 DoubleNav.debug.dylib`Navigation.path.setter(newValue=0 values) at ContentView.swift:86:22
    frame #1: 0x00000001043296d0 DoubleNav.debug.dylib`key path setter for Navigation.path : Navigation at <compiler-generated>:0
    frame #2: 0x000000019485b500 libswiftCore.dylib`Swift.NonmutatingWritebackBuffer.__deallocating_deinit + 132
    frame #3: 0x0000000194a351c4 libswiftCore.dylib`_swift_release_dealloc + 28
    frame #4: 0x0000000194a35bd4 libswiftCore.dylib`bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 156
    frame #5: 0x00000001d2f5f584 SwiftUICore`closure #1 () -> () in SwiftUI.ObjectLocation.set(_: τ_0_1, transaction: SwiftUI.Transaction) -> () + 132
    frame #6: 0x00000001d2f5f6b0 SwiftUICore`partial apply forwarder for closure #1 () -> () in SwiftUI.ObjectLocation.set(_: τ_0_1, transaction: SwiftUI.Transaction) -> () + 28
    frame #7: 0x00000001d2801da8 SwiftUICore`generic specialization <()> of closure #1 () throws -> τ_0_0 in SwiftUI.withTransaction<τ_0_0>(SwiftUI.Transaction, () throws -> τ_0_0) throws -> τ_0_0 + 296
    frame #8: 0x00000001d2f5f4d8 SwiftUICore`SwiftUI.ObjectLocation.set(_: τ_0_1, transaction: SwiftUI.Transaction) -> () + 336
    frame #9: 0x00000001d2c42468 SwiftUICore`SwiftUI.LocationBox.set(_: τ_0_0.Value, transaction: SwiftUI.Transaction) -> () + 148
    frame #10: 0x00000001d2c42c84 SwiftUICore`protocol witness for SwiftUI.Location.set(_: τ_0_0.Value, transaction: SwiftUI.Transaction) -> () in conformance SwiftUI.LocationBox<τ_0_0> : SwiftUI.Location in SwiftUI + 20
    frame #11: 0x00000001d2c42e90 SwiftUICore`SwiftUI.ProjectedLocation.set(_: τ_0_1.Projected, transaction: SwiftUI.Transaction) -> () + 196
    frame #12: 0x00000001d2c42468 SwiftUICore`SwiftUI.LocationBox.set(_: τ_0_0.Value, transaction: SwiftUI.Transaction) -> () + 148
    frame #13: 0x00000001d2cf2fe8 SwiftUICore`SwiftUI.Binding.ScopedLocation.set(_: τ_0_0, transaction: SwiftUI.Transaction) -> () + 28
    frame #14: 0x00000001d2c42468 SwiftUICore`SwiftUI.LocationBox.set(_: τ_0_0.Value, transaction: SwiftUI.Transaction) -> () + 148
    frame #15: 0x00000001d2cf3190 SwiftUICore`function signature specialization <Arg[0] = Owned To Guaranteed> of SwiftUI.Binding.wrappedValue.setter : τ_0_0 + 32
    frame #16: 0x00000001d2cf12c0 SwiftUICore`SwiftUI.Binding.wrappedValue.setter : τ_0_0 + 28
    frame #17: 0x00000001d1d0e538 SwiftUI`closure #1 () -> () in SwiftUI.NavigationColumnState.popAllForSelectionChange(popReplacedRoots: Swift.Bool) -> SwiftUI.NavigationState.RequestResults + 524
    frame #18: 0x00000001d281be58 SwiftUICore`partial apply forwarder for reabstraction thunk helper from @callee_guaranteed (@guaranteed Swift.Dictionary<__C.NSAttributedStringKey, Any>, @unowned __C._NSRange, @unowned Swift.UnsafeMutablePointer<ObjectiveC.ObjCBool>) -> () to @escaping @callee_guaranteed (@guaranteed Swift.Dictionary<__C.NSAttributedStringKey, Any>, @unowned __C._NSRange, @unowned Swift.UnsafeMutablePointer<ObjectiveC.ObjCBool>) -> () + 20
    frame #19: 0x00000001d2b3b64c SwiftUICore`static SwiftUI.Update.dispatchActions() -> () + 1080
    frame #20: 0x00000001d2b3ac4c SwiftUICore`static SwiftUI.Update.end() -> () + 108
    frame #21: 0x00000001d18daf44 SwiftUI`closure #1 (Swift.Optional<Swift.UnsafeMutableRawPointer>, Swift.Double, Swift.UnsafePointer<__C._UIUpdateTiming>) -> () in static SwiftUI.UIKitUpdateCycle.addPreCommitObserver(() -> ()) -> () + 168
    frame #22: 0x00000001d18dafbc SwiftUI`reabstraction thunk helper from @escaping @callee_guaranteed (@unowned Swift.Optional<Swift.UnsafeMutableRawPointer>, @unowned Swift.Double, @unowned Swift.UnsafePointer<__C._UIUpdateTiming>) -> () to @escaping @callee_unowned @convention(block) (@unowned Swift.Optional<Swift.UnsafeMutableRawPointer>, @unowned Swift.Double, @unowned Swift.UnsafePointer<__C._UIUpdateTiming>) -> () + 64
    frame #23: 0x0000000185030388 UIKitCore`_UIUpdateSequenceRun + 76
    frame #24: 0x00000001859d22e8 UIKitCore`schedulerStepScheduledMainSection + 168
    frame #25: 0x00000001859d1720 UIKitCore`runloopSourceCallback + 80
    frame #26: 0x000000018041b324 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #27: 0x000000018041b26c CoreFoundation`__CFRunLoopDoSource0 + 172
    frame #28: 0x000000018041a9d0 CoreFoundation`__CFRunLoopDoSources0 + 232
    frame #29: 0x00000001804150b0 CoreFoundation`__CFRunLoopRun + 788
    frame #30: 0x0000000180414960 CoreFoundation`CFRunLoopRunSpecific + 536
    frame #31: 0x0000000190183b10 GraphicsServices`GSEventRunModal + 160
    frame #32: 0x0000000185aa2b40 UIKitCore`-[UIApplication _run] + 796
    frame #33: 0x0000000185aa6d38 UIKitCore`UIApplicationMain + 124
    frame #34: 0x00000001d1e2eab4 SwiftUI`closure #1 (Swift.UnsafeMutablePointer<Swift.Optional<Swift.UnsafeMutablePointer<Swift.Int8>>>) -> Swift.Never in SwiftUI.KitRendererCommon(Swift.AnyObject.Type) -> Swift.Never + 164
    frame #35: 0x00000001d1e2e7dc SwiftUI`SwiftUI.runApp<τ_0_0 where τ_0_0: SwiftUI.App>(τ_0_0) -> Swift.Never + 84
    frame #36: 0x00000001d1b70c8c SwiftUI`static SwiftUI.App.main() -> () + 148
    frame #37: 0x0000000104333df0 DoubleNav.debug.dylib`static DoubleNavApp.$main() at <compiler-generated>:0
    frame #38: 0x0000000104333ea0 DoubleNav.debug.dylib`main at DoubleNavApp.swift:11:8
    frame #39: 0x00000001048f9410 dyld_sim`start_sim + 20
    frame #40: 0x000000010440e154 dyld`start + 2476

Thanks for any tips!

Hi @Bolsinga , well, I think that you should try to decouple the navigation on single list section. Probably on iPad simulator on regular size class (landscape) , the first and second section are seen on SwiftUI has two different navigation stack, then when you change the section, SwiftUI itself reset the navigation and move on root navigation stack. On compact size class as iPhone portrait mode, you see a reset on section but not moving on , because probably you should change the navigation stack root itself.

If you want to maintain the path previously stack navigation, you should use some sort of restore navigation and manage the root navigation itself. try to use the selection variable in your example and build from it the correct path with a binding.

Bye Rob

NavigationStack $path cleared on dealloc?
 
 
Q