Summary
When presenting a child coordinator that owns its own navigation state (FlowStack with a FlowPath / typed routes binding) via SwiftUI’s .fullScreenCover (or .sheet), the child inherits the parent FlowStack’s environment — including parentNavigationStackType and flowStackDataType. If the child then pushes screens, FlowStacks triggers the nesting assertion on iOS 16+ when the parent uses NavigationStack:
Unsupported nesting of FlowStacks. It is not possible to push from a child FlowStack when the navigation stack
is owned by the parent, where the child is also managing its own state...
FlowStacks already avoids this when using navigator.presentCover / presentSheet, because View+cover.swift clears the parent context:
content: { destination.environment(.parentNavigationStackType, nil) }
There is no public equivalent for app code that must use SwiftUI’s presentation modifiers (e.g. reusable ViewModifiers that are not inside a single parent coordinator and do not have access to FlowPathNavigator).
Use case
We have a reusable patient-picker modifier used across multiple screens. The UX requires:
Dismiss a bottom popup first
Then present a full-screen identity-verification flow in onDismiss
This choreography is natural with SwiftUI .fullScreenCover + @State, but cannot easily be replaced by router.presentCover because the modifier is attached to arbitrary host views and does not own a parent FlowPathNavigator.
Simplified reproduction pattern:
struct FFSelectPatientPopupModifier: ViewModifier {
@Binding var isPresented: Bool
@State private var isCreateRelativePresented = false
func body(content: Content) -> some View {
content
.presentPopupView(
isPresented: $isPresented,
onDismiss: {
// After popup dismisses, present full-screen flow
isCreateRelativePresented = true
},
content: { /* patient picker */ }
)
.fullScreenCover(isPresented: $isCreateRelativePresented) {
GIGlobalIdentityFlow(
onExit: {
isCreateRelativePresented = false
},
onCompleted: { patient in
onPatientPicked(patient)
isCreateRelativePresented = false
}
)
}
}
}
GIGlobalIdentityFlow is a coordinator using FlowStack($path, withNavigation: true) (own FlowPath state) and calls pushScreen for subsequent screens (e.g. scan ID → review). That matches Approach 2 (independent child state) in Nesting FlowStacks, but the child is presented via SwiftUI cover instead of presentCover, so the parent navigation context is not cleared.
Expected behavior
A child coordinator with its own FlowPath / routes binding, presented modally via SwiftUI .fullScreenCover or .sheet, should be able to push screens without nesting assertions — equivalent to presenting via FlowStacks presentCover.
Actual behavior
When the host view is inside a parent FlowStack (especially with NavigationStack on iOS 16+ / default useNavigationStack policy on iOS 26+), the child inherits parentNavigationStackType == .navigationStack and parentFlowStackDataType != nil. On first push, FlowStacks fires assertionFailure in FlowStack.swift (condition includes !deferToParentFlowStack).
Summary
When presenting a child coordinator that owns its own navigation state (FlowStack with a FlowPath / typed routes binding) via SwiftUI’s .fullScreenCover (or .sheet), the child inherits the parent FlowStack’s environment — including parentNavigationStackType and flowStackDataType. If the child then pushes screens, FlowStacks triggers the nesting assertion on iOS 16+ when the parent uses NavigationStack:
Unsupported nesting of FlowStacks. It is not possible to push from a child FlowStack when the navigation stack
is owned by the parent, where the child is also managing its own state...
FlowStacks already avoids this when using navigator.presentCover / presentSheet, because View+cover.swift clears the parent context:
content: { destination.environment(.parentNavigationStackType, nil) }
There is no public equivalent for app code that must use SwiftUI’s presentation modifiers (e.g. reusable ViewModifiers that are not inside a single parent coordinator and do not have access to FlowPathNavigator).
Use case
We have a reusable patient-picker modifier used across multiple screens. The UX requires:
Dismiss a bottom popup first
Then present a full-screen identity-verification flow in onDismiss
This choreography is natural with SwiftUI .fullScreenCover + @State, but cannot easily be replaced by router.presentCover because the modifier is attached to arbitrary host views and does not own a parent FlowPathNavigator.
Simplified reproduction pattern:
GIGlobalIdentityFlow is a coordinator using FlowStack($path, withNavigation: true) (own FlowPath state) and calls pushScreen for subsequent screens (e.g. scan ID → review). That matches Approach 2 (independent child state) in Nesting FlowStacks, but the child is presented via SwiftUI cover instead of presentCover, so the parent navigation context is not cleared.
Expected behavior
A child coordinator with its own FlowPath / routes binding, presented modally via SwiftUI .fullScreenCover or .sheet, should be able to push screens without nesting assertions — equivalent to presenting via FlowStacks presentCover.
Actual behavior
When the host view is inside a parent FlowStack (especially with NavigationStack on iOS 16+ / default useNavigationStack policy on iOS 26+), the child inherits parentNavigationStackType == .navigationStack and parentFlowStackDataType != nil. On first push, FlowStacks fires assertionFailure in FlowStack.swift (condition includes !deferToParentFlowStack).