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 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 code works fine in Xcode 12 / iOS 14.4



Here is a sample code.

Code Block swift
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()
  }
}






Post not yet marked as solved Up vote post of JanC Down vote post of JanC
51k views
  • The WA works. But this is still present in 14.7.

Add a Comment

Replies

empty navigationlink works for me also. I am using navigation links inside of VStacks and HStacks. This bug is potentially catastrophic for so many developers. Is there an Apple representative who is responding on this thread? I am new to the developer forum.

I was able to fix my problem by changing the navigationViewStyle to StackNavigationViewStyle in the main navigation view. It doesn't use the split view which is not a big deal for my app.

NavigationView {
      VStack(alignment: .leading) {
        VStack(alignment: .leading, spacing: 20) {
          Text("choose a topic to draw: ").font(Font.extraLight17)
          HStack(alignment: .center, spacing: 18) {
            NavigationLink(destination: LibraryView("lineArt", title: "Line Arts")) {
              TopicCell(imageName: "cellLineArt", title: "Line Arts")
            }.buttonStyle(PlainButtonStyle())
            NavigationLink(destination: TextToDrawView()) {
              TopicCell(imageName: "header", title: "Headlines")
            }.buttonStyle(PlainButtonStyle())
          }
          HStack(alignment: .center, spacing: 18) {
            NavigationLink(destination: LibraryView("banner", title: "Banners")) {
              TopicCell(imageName: "cellBanner", title: "Banners")
            }.buttonStyle(PlainButtonStyle())
            NavigationLink(destination: LibraryView("reference", title: "References")) {
              TopicCell(imageName: "cellReference", title: "Refences")
            }.buttonStyle(PlainButtonStyle())
          }
        }.padding()
         
        NavigationLink(destination: VectorizeDrawView()) {
          BlackButtonView(title: "Select to Draw")
        }
        .buttonStyle(PlainButtonStyle())
        .padding()
      }
    }.navigationViewStyle(StackNavigationViewStyle())

example

  • The ".navigationViewStyle(StackNavigationViewStyle())" was really useful for me thanks a lot

  • Thank you, this is what helped me in my case! Cheers!

  • Thank you! It reminds me of that this is the exact way how it is working on vertical iPhone. Thanks for sharing this finding!

Add a Comment

We were running into the same issue. Wow. Our app was in production and everything was fine, after an iOS update the app becomes unusable for some users.

The

NavigationLink(destination: EmptyView()) {
    EmptyView()
}

hack resolved it for me. I would really appreciate if an Apple engineer could have a look at this. Happy to provide more information.

  • Where did you put it though?

Add a Comment

I've run into a similar issue (xCode 12.5.1, iOS 14):

I've built the tiny app below, to reproduce this: a Game class that can increment a counter, and a GameView that calls this functionality. Nothing else. The ContentView 'owns' the Game class and passes it to the GameView in its NavigationLink. This works flawlessly, unless:

  • There are exactly two links in the ContentView (which I saw others mention)
  • At least one of them is bound to a @State var with tag/selection (even if it doesn't use it :/ )

In that case clicking the "Increment Count" button will pop the view. Removing the tag/selection from the second link, or adding the EmptyView will both fix it:

struct ContentView: View {
   
  @StateObject var game = Game()
   
  @State var action: Int? = 0
   
  var body: some View {
    NavigationView {
      VStack {
        NavigationLink(destination: GameView(game: game)) {
          Text("Link 1")
        }

        NavigationLink(destination: Text("Useless View"), tag: 1, selection: self.$action) {
          Text("Useless Link")
        }
         
        //NavigationLink(destination: EmptyView()) {
        //  EmptyView()
        //}
      }
    }
  }
}

struct GameView: View {
  @ObservedObject var game: Game
   
  var body: some View {
    VStack{
      Text("Count: \(game.count)")
      Button("Increment Count") {
        game.incrementCount()
      }
    }
  }
}

class Game: ObservableObject {
  @Published var count: Int = 0

  func incrementCount() {
    count += 1
  }
}

@TheXs2490

How would this work if View1 looked like this:

struct View1: View{

  var body: some View{

       LazyVGrid(columns: gridLayout, spacing: 10) {
          ForEach(Array(person.item! as! Set<Item>).sorted { $0.date! > $1.date! }, id: \.self) { (item: Item) in
         NavigationLink(destination: View2(){
             Text("Navigate")
             
           }
         }
     }
  }
}

I can't get it to compile properly if the if self.KeepView2 is right before the NavigationLink. And if I put it before the LazyVGrid, it won't display the contents of the ForEach Array. Thanks!

Hey, I encountered the same "bug" but in fact I don't see any bug in my app, just that weird log. And finally it even happens with the SwiftUI tutorial from Apple https://developer.apple.com/tutorials/swiftui when navigating in the views.

Hope this helps. Working with NavigationViews and NavigationLinks, we've found that having State on a parent that can be mutated by several levels of children views, which have conditional NavigationLinks create autopop and autopush issues. This seems to be releated on how SwiftUI tries to uniquely identify those NavigationLink components. Here's an example:

The app contains a shared State

struct TestNavigationApp: App {
    @StateObject var viewModel = DummyViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(viewModel)
        }
    }
}

The ContentView has a NavigationLink which leads to a loop of navigations

struct ContentView: View {
    @EnvironmentObject var viewModel: DummyViewModel
    @State var autopopActive = false

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: AutopopLevel, isActive: $autopopActive) {
                    Button {
                        viewModel.level = 1
                        autopopActive = true
                    } label: {
                        Text("Autopop level")
                    }
                    .padding()
                }
            }
            .navigationTitle("Main")
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }

    var AutopopLevel: some View {
        AutopopLevelView()
            .environmentObject(viewModel)
    }
}

struct AutopopLevelView: View {
    @EnvironmentObject var viewModel: DummyViewModel
    @State var isPresented = false
    @State var isActive = false
    var body: some View {
        if viewModel.level == 3 {
            Button {
                isPresented = true
            } label: {
                Text("Fullscreen cover")
            }
            .fullScreenCover(isPresented: $isPresented) {
                VStack {
                    HStack {
                        Button {
                            isPresented = false
                        } label: {
                            Text("Close")
                        }
                        .padding()
                        Spacer()
                    }
                    Spacer()
                    Text("Fullscreen")
                    Spacer()
                }
            }
        } else {
            NavigationLink(destination: Level, isActive: $isActive) {
                Button {
                    viewModel.level += 1
                    isActive = true
                } label: {
                    Text(viewModel.level == 3 ? "Fullscreen cover" : "Level \(viewModel.level)")
                }
            }
        }
    }

    var Level: some View {
        AutopopLevelView()
            .environmentObject(viewModel)
    }
}

As you can see, AutopopLevelView has a NavigationLink conditional to the shared viewModel, which leads to an autopop. To fix that, we did the following:

struct CorrectLevelView: View {
    @EnvironmentObject var viewModel: DummyViewModel
    @State var isPresented = false
    @State var isActive = false

    var body: some View {
        NavigationLink(destination: Level, isActive: $isActive) {
            Button {
                if viewModel.level == 3 {
                    isPresented = true
                } else {
                    viewModel.level += 1
                    isActive = true
                }
            } label: {
                Text(viewModel.level == 3 ? "Fullscreen cover" : "Level \(viewModel.level)")
            }
        }
        .navigationTitle("Level \(viewModel.level)")
        .fullScreenCover(isPresented: $isPresented) {
            VStack {
                HStack {
                    Button {
                        isPresented = false
                    } label: {
                        Text("Close")
                    }
                    .padding()
                    Spacer()
                }
                Spacer()
                Text("Fullscreen")
                Spacer()
            }
        }
    }

    var Level: some View {
        CorrectLevelView()
            .environmentObject(viewModel)
    }
}

Must say that this is only one of the issues that we found. To fix how to work with NavigationLinks, I would recommend to:

  • Avoid having NavigationLinks inside conditions
  • Not using NavigationLinks as buttons, and using isActive to specifically have more control over it
  • Apple is showcasing simple projects when you usually don't have more than 1 level deepness on NavigationLinks, so they can use the NavigationLink(destination: <>, label: <>) initializer, but we've found that is not a good idea.
  • Avoid using NavigationLink(destination: <>, tag: <>, selection: <>, label: <>) if possible, we got some weird behaviours with it.
  • Not using more than 1 NavigationLink per body view and just setting the destination conditionally, that way you have full control over it and you can decide which View you want as destination. Example: if you have a list of 6 map views, create 6 Buttons inside the List / VStack / whatever list UI component and just put 1 NavigationLink at the body level, which will have a different destination View based on some conditions, example:
var body: some View {
    VStack {
        ForEach(maps) { map in
            Button {
                // Here you want to change the conditional binding
                // and the condition that choses the view
                modifyConditionals()
            } label: {
                Text(map.name)
            }
            .padding()
        }
    }
    .navigationTitle("Maps")

    NavigationLink(destination: destinationMapView, isActive: $someConditionalBinding) {
        EmptyView()
    }
}

@ViewBuilder
var destinationMapView: some View {
    switch someCondition {
    case .someCase:
        return MapView()
    //...
    }
}
  • I would like to have, if possible, some comments on this from some Apple official support technician, eskimo maybe?

Add a Comment

After spending days debugging this same issue in my SwiftUI app I’ve come across this thread. I’m half relieved that I’m not alone experiencing this issue, and that it’s not necessarily my implementation at fault. But also horrified slightly that this very severe issue has not been resolved by apple after several releases of iOS.

I will be trying the workaround moving forward, but echo others comments in this thread, I would appreciate some support from apple on this one.

🍏🍏⛔️⛔️🚧🚧

is SwiftUI production ready? 🤷‍♂️

I'm having the same problem with my iPhone-App (otherwise ready for submission to the store). The only workaraound that worked for me was the one with the EmptyView. I had no success with .isDetailLink(false) nor with StackNavigationViewStyle.

Now I have updated my iPhone from iOS 14.x to iOS 15.0.2 and XCode to version 13.0. With the updates, the bug no longer occurs in the very situations it previously occurred in, however, it now occurs in other situations.

Unfortunately, the EmptyView-workaraound is no longer an option, since, with the update to iOS 15, EmptyView creates an empty but visible row in the parent view.

I can't believe something as basic as NavigationLink is still broken...

Are there any new workaround suggestions?

I fixed this issue adding StackNavigationViewStyle with navigationViewStyle modifier to NavigationView parent

NavigationView { .... }.navigationViewStyle(StackNavigationViewStyle())

Happy coding!

  • Thank you. I wasted an entire day trying to find a non-existent bug in my code before finding this comment. Adding this one modifier to the Navigation view's parent fixed everything.

Add a Comment

For me, my NavigationLinks were popping out whenever the app enters background, e.g. going back to iOS home screen or activating Control Center.

I had found that I had an idle @Environment(\.scenePhase) var scenePhase in my App struct, that seem to be causing the uncommanded popping of the NavigationLink. Once I removed the scenePhase the problem was gone.

I could never find a reliable solution to this horrible bug. So I decided to create a custom NavigationLink, using Introspect (https://github.com/siteline/SwiftUI-Introspect). This works way better than expected, because all swiftui related functions continue working as usual. Seems like the bug is specifically with NavigationLink.

private struct NavigationLinkImpl<Destination: View, Label: View>: View {
    let destination: () -> Destination?
    @State var isActive = false
    @ViewBuilder let label: () -> Label

    var body: some View {
        NavigationLinkImpl1(destination: destination, isActive: $isActive, label: label)
    }
}

private struct NavigationLinkImpl1<Destination: View, Label: View>: View {
    let destination: () -> Destination
    @Binding var isActive: Bool
    @ViewBuilder let label: () -> Label
    @State var model = Model()

    var body: some View {
        Button(action: action, label: label)
            .introspectNavigationController(customize: handle)
            .id(isActive)
    }

    func handle(nav: UINavigationController) {
        if isActive {
            if model.destination == nil {
                let dest = UIHostingController<Destination>(rootView: destination())
                nav.pushViewController(dest, animated: true)
                model.destination = dest
            }
        } else {
            if let dest = model.destination {
                if let i = nav.viewControllers.lastIndex(of: dest) {
                    nav.setViewControllers(.init(nav.viewControllers.prefix(i + 1)), animated: true)
                }
                model.destination = nil
            }
        }
        if isActive != model.contains(nav: nav) { // detect pop
            isActive = model.contains(nav: nav)
        }
    }

    final class Model {
        var destination: UIHostingController<Destination>?
        func contains(nav: UINavigationController) -> Bool { destination.map { nav.viewControllers.contains($0) } ?? false }
    }

    func action() { isActive = true }
}

extension NavigationLink {
    init<Destination: View, Label: View>(destination: @autoclosure @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) {
        self.init(body: NavigationLinkImpl(destination: destination, label: label))
    }

    init<Destination: View, Label: View>(destination: @autoclosure @escaping () -> Destination, isActive: Binding<Bool>, @ViewBuilder label: @escaping () -> Label) {
        self.init(body: NavigationLinkImpl1(destination: destination, isActive: isActive, label: label))
    }

    init<Destination: View>(_ text: String, destination: @autoclosure @escaping () -> Destination, isActive: Binding<Bool>) {
        self.init(destination: destination(), isActive: isActive) { Text(text) }
    }

    init<Destination: View>(_ text: String, destination: @autoclosure @escaping () -> Destination) {
        self.init(destination: destination()) { Text(text) }
    }
}

Put this in a file, and your existing NavigationLinks will work just fine. Tested in ios 14 and 15

UIViewControllerRepresentable also causes problems with NavigationLink inside. Try to use UIViewRepresentable instead of UIViewControllerRepresentable if you have the same issue.

adding .navigationViewStyle to NavigationView fixed the issue for me:

NavigationView {
       
    }
    .navigationViewStyle(StackNavigationViewStyle())
  • Thank you!!! This fixed a similar issue for me!

Add a Comment