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()
  }
}






What's sad is the navigation workarounds don't work some scenarios so my plan C in those cases has been to use a .sheet instead of navigating, which takes SwiftUI mediocrity to a whole new level
There is another similar bug. When you are in the parent view and active a navigationView with the .isDetailLink(false) call, after you return from it, when you try to active another navigationLink with or without .isDetailLink(false) call, It wont be presented. And you will get the error message "Unable to present. Please file a bug." in the console.

Any help?

Adding a NavigationLink with an empty view didn't work for me. I solved my issue removing all NavigationLinks from the ForEach and using a single one to control the navigation to the detail view, a tap gesture and 2 state variables to keep track on what is being tapped on.

The example broken code and fix can be found at Paul Hudson's site.

https://www.hackingwithswift.com/forums/swiftui/unable-to-present-please-file-a-bug/7901/8237

Below is the complete working version

import SwiftUI

struct NavigationViewOptions {
    enum OptionType { case main, optional }
    typealias Option = (id: UUID, value: String, type: Self.OptionType)
    static var options: [Option] = [
        (UUID(), "Option 1", .main),
        (UUID(), "Option 2", .optional),
        (UUID(), "Option 3", .main),
        (UUID(), "Option 4", .main),
        (UUID(), "Option 5", .optional),
    ]
        
    static func buildView(for option: Option) -> some View {
        switch option.type {
        case .main:
            return Text("Main Option selected\n\(option.value)").font(.title).fontWeight(.bold)
        case .optional:
            return Text("Optional Option selected\n\(option.value)").font(.title3).italic().fontWeight(.medium)
        }
    }
}

struct NavigationViewWorking: View {

    // State variables to leep track of what option has been tapped on and when to navigate to new view
    @State private var selectedOption: NavigationViewOptions.Option = (id:UUID(),"",.main)
    @State private var showDetail: Bool = false
    
    var body: some View {
        NavigationView {
            ScrollView{
                VStack (alignment:.leading) {
                    Text("NAVIGATION FIX FOR:\nUnable to present. Please file a bug.")
                        .padding(.bottom, 40)

                    ForEach(NavigationViewOptions.options, id: \.id) { option in
                        Text(option.value)
                            .font(.title)
                            .padding(.vertical, 10)
                            .foregroundColor(.accentColor) // same color as navigationLink
                            // handle tap on option
                            .onTapGesture {
                                selectedOption = option
                                showDetail = true
                            }
                    }
                    Spacer()
                    NavigationLink("", destination: NavigationViewOptions.buildView(for: selectedOption), isActive: $showDetail)
                        .opacity(0)
                }
                .navigationTitle("Options")
            }
            // INITIAL DETAIL VIEW
            Text("Select option from the left")
        }
    }
}

I was seeing this issue in a few places in my app - empty navigation workaround solved it where I had NavigationLinks, but not where I simply presented views. Trying this in Xcode 13 beta 1, I'm not seeing the issue. Anyone else tried the new beta yet?

I've been struggling with this, and user state controlled navigationlink is still not working correctly in Xcode 14.5.1.

I have some views with almost identical functionality, some work, some do not.

What I have discovered is that the views that are the first level of the Navigation stack do not work correctly, but those further down for some reason do work correctly.

NavigationView -> View (with User State Links) // DOES NOT WORK

NavigationView -> NavigationLink -> View (with User State Links) // WORKS

This code demonstrates the problem, and how I have managed to get it working for now by wrapping the view in a container link.

import SwiftUI

let data = ["Item A", "Item B", "Item C"]

struct ContentView: View {
    var body: some View {
        TabView {
            FirstTabView().tabItem {
                Image(systemName: "1.circle.fill")
                Text("Not working")
            }

            SecondTabContainerView().tabItem {
                Image(systemName: "2.circle.fill")
                Text("Working")
            }
        }
    }
}

// First Tab View  ** NOT WORKING **

struct FirstTabView: View {
    @State private var showingDetailView = false

    var body: some View {
        NavigationView {
            List {
                ForEach(data, id: \.self) { data in
                    NavigationLink(destination: DetailView(data: data)) {
                        Text(data)
                    }
                }
            }
            .navigationBarItems(trailing: Button(action: {
                showingDetailView = true
            }) { Text("Next Page") })

            NavigationLink(destination: DetailView(data: "Next Page"), isActive: $showingDetailView) {
                EmptyView()
            }
        }
    }
}

struct DetailView: View {
    var data: String
    var body: some View {
        Text(data)
    }
}

// Second Tab Views ** WORKING **

struct SecondTabContainerView: View {
    @State private var showingDetailView = true

    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondTabView(), isActive: $showingDetailView) { EmptyView() }
        }
    }
}

struct SecondTabView: View {
    @State private var showingDetailView = false

    var body: some View {
        List {
            ForEach(data, id: \.self) { data in
                NavigationLink(destination: DetailView(data: data)) {
                    Text(data)
                }
            }
        }
        .listStyle(InsetGroupedListStyle())
        .navigationBarItems(trailing: Button(action: {
            showingDetailView = true
        }) { Text("Next Page") })
        .navigationBarBackButtonHidden(true)

        NavigationLink(destination: DetailView(data: "Next Page"), isActive: $showingDetailView) {
            EmptyView()
        }
    }
}

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

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.

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()
    //...
    }
}

The WA works. But this is still present in 14.7.

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!

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())

For the same app, I get this error running on a iOS 14.8 device but I do NOT get the error on a 15.3.1 device.

Maybe I found the reason of this bug... if you use iOS 15 (not found iOS 14), and you write the code NavigationLink to go to same View in different locations in your projects, then this bug appear. So I simply made another View that has different destination View name but the same contents... then it works.. you can try.... sorry for my poor English...

I had to set isDetailLink(false) to the NavigationLinks in order to stop this behaviour.

I was having this issue because i used

@State var date = Date() 

i removed it and the issue is no more there.

this solution did not work in my case : NavigationLink(...){ emptyView() }

SwiftUI NavigationLink pops out by itself
 
 
Q