I have a simple use case where a screen pushes another screen using the NavigationLink
. There is a strange behaviour iOS 14.5 beta (1, 2, 3), where the pushed screen is popped just after being pushed.
I manage to create a sample app where I reproduce it. I believe the cause is the presence of @Environment(\.presentationMode)
that seem to re-create the view and it causes the pushed view to be popped.
The exact same cod works fine in Xcode 12 / iOS 14.4
Here is a sample code.
import SwiftUI
public struct FirstScreen: View {
public init() {}
public var body: some View {
NavigationView {
List {
row
row
row
}
}
}
private var row: some View {
NavigationLink(destination: SecondScreen()) {
Text("Row")
}
}
}
struct SecondScreen: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
public var body: some View {
VStack(spacing: 10) {
NavigationLink(destination: thirdScreenA) {
Text("Link to Third Screen A")
}
NavigationLink(destination: thirdScreenB) {
Text("Link to Third Screen B")
}
Button("Go back", action: { presentationMode.wrappedValue.dismiss() })
}
}
var thirdScreenA: some View {
Text("thirdScreenA")
}
var thirdScreenB: some View {
Text("thirdScreenB")
}
}
struct FirstScreen_Previews: PreviewProvider {
static var previews: some View {
FirstScreen()
}
}
I just added .navigationViewStyle(StackNavigationViewStyle())
& bug vanishes
Example :--
NavigationView {
content
}
.navigationViewStyle(StackNavigationViewStyle())
Looks like a bug when there is exactly 2 NavigationLinks. If you add another empty link it goes away:
NavigationLink(destination: EmptyView(), label: {})
More details: https://forums.swift.org/t/14-5-beta3-navigationlink-unexpected-pop/45279
Applying .isDetailLink(false)
to your NavigationLink may do the job. By default it is true. From the docs:
This method sets the behavior when the navigation link is used in a multi-column navigation view, such as DoubleColumnNavigationViewStyle. If isDetailLink is true, performing the link in the primary column sets the contents of the secondary (detail) column to be the link’s destination view. If isDetailLink is false, the link navigates to the destination view within the primary column.
In my case I had:
(1) A tab view with a root view that had 2 navigation starting points
(2) the first and second navigations have quite a lot of nested views
(3) adding .navigationViewStyle(StackNavigationViewStyle() fixed the issue only for root views that had only one starting point of navigation
(4) as soon as I had another navigation coming from the same view the problem reappeared (only for iOS 14.5 to 14.8)
(5) solely adding the
NavigationLink(destination: EmptyView()) {
EmptyView()
}
didn't work for me
(6) On my project I have a coordinator that is responsible for creating the viewModels (that are published properties) and I have a ContainerView that will handle all the navigation. The navigation links are created based on the existence or not of a viewModel, so if a viewModel exists that view will be presented and when the view is dismissed the viewModel will be set to nil. (I'll add the code that does that at the end)
(7) for some weird reason, adding a third navigation to the view that is responsible for the navigation stopped my container view to be re-rendered and the view to stop being popped back.
Simply adding the NavigationLink to the container didn't work, but using the modifier I'm using and setting the destination to be the Empty NavigationLink did.
This is the code that is used for the navigation:
func navigation<Item, Destination: View>(
item: Binding<Item?>,
@ViewBuilder destination: (Item) -> Destination
) -> some View {
let isActive = Binding(
get: { item.wrappedValue != nil },
set: { value in
if !value {
item.wrappedValue = nil
}
}
)
return navigation(isActive: isActive) {
item.wrappedValue.map(destination)
}
}
func navigation<Destination: View>(
isActive: Binding<Bool>,
@ViewBuilder destination: () -> Destination
) -> some View {
overlay(
NavigationLink(
destination: isActive.wrappedValue ? destination() : nil,
isActive: isActive,
label: { EmptyView() }
)
)
}
This is how my container view works. My coordinator has Published properties that are optional viewModels and the existence or not of that viewModel is what triggers the isActive value on the navigation link. Adding that last .navigation worked. My empty container just has the empty navigationLink
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With