Jan
Jan

Reputation: 7644

SwiftUI NavigationLink pops out by itself

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

enter image description here

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

Answers (6)

Deepansh Jagga
Deepansh Jagga

Reputation: 109

change NavigationView { content } to NavigationStack { content }

Upvotes: 1

John Gorenfeld
John Gorenfeld

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.

  1. The link itself was destroyed after you clicked it. Obvious: If you are using NavigationLinks that rely on inActive to decide if they are pushing or not, make sure the Bool they depend on doesn't change somewhere in a child view. Less obvious: That link you tapped on to get on here. Has it stopped existing? If so, you pop back!

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())
}
  1. Competing navigation links. Be careful when creating multiple Navigation Links, e.g. in a list, that closely resemble each other, share variables, or compete for a single isActive binding variable. Strange popback behavior can result from, for example, a list of 5 items that each have their own NavigationLink where all of them are depending on the same variable, $shouldPush.

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

Jiropole
Jiropole

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

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

My navigation container

Upvotes: 5

Jack
Jack

Reputation: 14369

I just added .navigationViewStyle(StackNavigationViewStyle()) & bug vanishes

Example :--

 NavigationView {
         content
}
.navigationViewStyle(StackNavigationViewStyle())

Upvotes: 40

Paul Budzinsky
Paul Budzinsky

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

Related Questions