Skip to content

Introduce NavigationStackBound as a fix for unpredictable ContainerLifecycle calls#107

Merged
Maks-Jago merged 9 commits into
1.5.1from
feature/navigation-stack-bound
May 19, 2026
Merged

Introduce NavigationStackBound as a fix for unpredictable ContainerLifecycle calls#107
Maks-Jago merged 9 commits into
1.5.1from
feature/navigation-stack-bound

Conversation

@killlilwinters
Copy link
Copy Markdown
Collaborator

@killlilwinters killlilwinters commented Apr 17, 2026

Description

This merge request introduces a dedicated NavigationStackBound SwiftUI wrapper to fix unstable NavigationStack behavior when the navigation path is exposed from UDF state through reconstructed Binding(get:set:) instances.

The issue occurs because NavigationStack is sensitive to binding identity. When a container remaps the path binding on every state update, SwiftUI may re-traverse the navigation tree during active transitions, causing unintended descendant lifecycle effects such as duplicate @StateObject initialization and incorrect onContainerDidLoad / onContainerDidUnload calls.

Instead of moving navigation state ownership away from UDF, this approach keeps the external binding as the source of truth and stabilizes only the binding presented to NavigationStack. The alternative would have been to redesign navigation ownership or add explicit synchronization flags, but this solution isolates the problem at the view boundary with minimal impact on existing state flow.

Changes Made

  • Added new NavigationStackBound<Root: View> component in UDF/View/Router/NavigationStackBound.swift.
  • Wrapped an external Binding<NavigationPath> with:
    • @Binding private var external
    • @State private var local
  • Seeded local state from the external path during initialization so restored/deep-linked navigation appears immediately without an empty initial stack.
  • Rendered NavigationStack using the stable local binding:
    NavigationStack(path: $local) {
        root()
    }
  • Added two-way synchronization between external and local paths using equality guards to prevent update loops:
    .onChange(of: external) { newExternal in
        guard local != newExternal else { return }
        local = newExternal
    }
    .onChange(of: local) { newLocal in
        guard external != newLocal else { return }
        external = newLocal
    }
  • Documented the failure mode, synchronization model, usage pattern, and an important requirement that the owning container scope must include the form that stores the navigation path.

@killlilwinters killlilwinters marked this pull request as ready for review April 30, 2026 18:42
@killlilwinters killlilwinters added the bug Something isn't working label Apr 30, 2026
@killlilwinters killlilwinters added this to the 1.5.1 milestone Apr 30, 2026
Comment thread UDF/View/Router/NavigationStackBound.swift
Comment thread UDF/View/Router/NavigationStackBound.swift Outdated
@Sashe0 Sashe0 removed the request for review from Devepre May 19, 2026 11:07
@Maks-Jago Maks-Jago merged commit 9a16373 into 1.5.1 May 19, 2026
1 check passed
@Maks-Jago Maks-Jago deleted the feature/navigation-stack-bound branch May 19, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants