Reputation: 7644
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()
}
}
Upvotes: 27
Views: 13490
Reputation: 109
change NavigationView { content }
to NavigationStack { content }
Upvotes: 1
Reputation: 2455
I have run into this behavior a lot in SwiftUI 1.0. Although the unwanted popbacks are maddening, there are some logical, but undocumented, pop-causing situations that can be eliminated without assuming first (as I did from other answers about later iOS versions) that it was a Swift bug. I want to share them here so other developers don't have to waste the time I did on this iOS13 pain point.
For example:
// This variable will pop you back if false.
// What if it's changed in @Binding somewhere down the hierarchy?
// What if it were an Environment Object that was being changed?
@State var modeForShowingButtons: Bool = true
if $modeForShowingButtons == true {
NavigationLink(myDestination())
}
I had a List with popback problems. I tried all of the fixes from Stack Overflow with no success until I simplified the NavigationLink hierarchy. Instead of letting every row create its own multiple Navigation Links that may have clashed with or contradicted each other, I created a single push @State in the parent view. The popbacks went away.
Hope this helps someone. These might seem super obvious. But if you were, as I was, used to a Storyboard world where you could push a segue and then forget about it, these might not have crossed your mind.
Upvotes: 4
Reputation: 174
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.
Upvotes: 11
Reputation: 181
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
Upvotes: 5
Reputation: 14369
I just added .navigationViewStyle(StackNavigationViewStyle())
& bug vanishes
Example :--
NavigationView {
content
}
.navigationViewStyle(StackNavigationViewStyle())
Upvotes: 40
Reputation: 269
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
Upvotes: 21