SwiftUI NavigationLink pops out by itself
Asked Answered
B

6

26

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()
    }
}
Belemnite answered 10/3, 2021 at 7:0 Comment(1)
This should not be an issue on iOS15 + (including iOS15)Annulet
D
21

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

Dietetic answered 15/4, 2021 at 8:11 Comment(9)
Where should the empty navigation link go? I have the same issue (although I have many more than 2 links) and adding the empty link only fixes the first one but all others are still poppingRefresher
@Refresher I've seen your comment on the Apple dev forums. It seems your'e facing different issue. If your DetailView updates data that is used in the ForEach list on MasterView and that causes Master to rerender then it might result in poping child view hierarchy. Please submit another question with more detailsDietetic
seem not working any more on iOS 15Annulet
Can confirm that this misbehaviour is present on the latest iOS 15 version (2021 9th of Nov.)Stucco
I'm having this issue as well with a sheet that shows a list of rows, so when a row is pressed the app should go to another view/screen and the sheet is closed, which is happening but the view/screen is popped up right after being pushed.Unapproachable
I don't know how did you figured that its causing only when we have 2 nav-links. But this strange solution worked for me. Thanks.Companionate
solution is here forums.swift.org/t/14-5-beta3-navigationlink-unexpected-pop/…Blah
Using the solution posted by @ArutyunEnfendzhyan the problem is solved BUT other issues are present thus nullifying it..Stucco
This is crazy! It worked, thanks!!Polyunsaturated
G
39

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

Example :--

 NavigationView {
         content
}
.navigationViewStyle(StackNavigationViewStyle())
Grillwork answered 7/10, 2021 at 16:18 Comment(10)
I just spent 4 hours on this bug. This is the only workaround that worked...Phenobarbitone
@QuentinHayot glad i could helpGrillwork
I'm having this issue as well with a sheet that shows a list of rows, so when a row is pressed the app should go to another view/screen and the sheet is closed, which is happening but the view/screen is popped up right after being pushed. I already added this property to the NV, but it’s not working on iOS 15.Unapproachable
This is the only working solution till Dec 1st, 2021Marchese
It doesn't work for me.Easeful
Works for me. Found it after spending 5 hours past midnightChittagong
@BenLu glad. it took 1 day for me so i posted for help other folks :)Grillwork
Works for me. Found it after spending 4 hours. This also works on latest iOS 15.4 Thank you so much!Ecumenism
Still working on 16.2Jinn
same code, but a little cleanerL .navigationViewStyle(.stack)Antiseptic
D
21

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

Dietetic answered 15/4, 2021 at 8:11 Comment(9)
Where should the empty navigation link go? I have the same issue (although I have many more than 2 links) and adding the empty link only fixes the first one but all others are still poppingRefresher
@Refresher I've seen your comment on the Apple dev forums. It seems your'e facing different issue. If your DetailView updates data that is used in the ForEach list on MasterView and that causes Master to rerender then it might result in poping child view hierarchy. Please submit another question with more detailsDietetic
seem not working any more on iOS 15Annulet
Can confirm that this misbehaviour is present on the latest iOS 15 version (2021 9th of Nov.)Stucco
I'm having this issue as well with a sheet that shows a list of rows, so when a row is pressed the app should go to another view/screen and the sheet is closed, which is happening but the view/screen is popped up right after being pushed.Unapproachable
I don't know how did you figured that its causing only when we have 2 nav-links. But this strange solution worked for me. Thanks.Companionate
solution is here forums.swift.org/t/14-5-beta3-navigationlink-unexpected-pop/…Blah
Using the solution posted by @ArutyunEnfendzhyan the problem is solved BUT other issues are present thus nullifying it..Stucco
This is crazy! It worked, thanks!!Polyunsaturated
F
10

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.

Foxhound answered 17/12, 2021 at 21:8 Comment(3)
This worked like a charm. This should be the right answer.Haase
It does not work for me. This whole behaviour is very weird.Delibes
This worked for me as expected, must be the selected answer.Scudo
S
5

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

Smoodge answered 9/11, 2021 at 12:48 Comment(4)
Try removing the coordinator and view models, really weird to use those with SwiftUILights
@Lights why do you think it's weird? How would you decouple the navigation from the view in SwiftUI? I'm not sure if you have a very deep nested navigation to work with in SwiftUI, but my project is quite big and having navigation links inside views and having to pop to different views based on actions was quite difficult using the navigation links inside views.Smoodge
combine related properties into their own struct and use mutating func for actions. Pass it down via @Binding. See WWDC 2020 Data Essentials in SwiftUI from 4mins.Lights
Thank you! I am using the same navigation architecture and you just saved my day!Bligh
L
4

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.

Lovelace answered 14/3, 2023 at 14:32 Comment(1)
Thanks for #1, I had a if loading { <some view> } else { NavigationLink }type of setup. I added .background { NavigationLink } to the containing, always visible, view. This fixed it.Barram
R
0

change NavigationView { content } to NavigationStack { content }

Rufusrug answered 1/12, 2023 at 20:48 Comment(3)
Please add some explanation and if possible link some sources, it helps future readers understand, especially beginners who visit this site to learn.Dido
honestly I don't know why this works and why none of the other answers worked for me. I just know that .navigationViewStyle(StackNavigationViewStyle()) mentioned in another answer is deprecated by Apple, so I tried a variation of it and it worked.Rufusrug
That's actually helpful, please add that as explanation, and if possible, link a source that confirms its deprecation.Dido

© 2022 - 2025 — McMap. All rights reserved.