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

@coopersita
For us it worked placing them at the same level as the other NavigationLinks already present in the same view.


A simple workaround is to have one overlay for the navigationLinks and activate it conditionally.
Seeing this exact issue and can confirm that the empty navigation link does seem to resolve it for me too. I do see Unable to present. Please file a bug. in the console too.

This has retroactively broken our App Store build with no updates which is the most annoying part..
For me adding the
Code Block
NavigationLink(destination: EmptyView()) {
  EmptyView()
}

still created a weird behaviour depending on the number of NavigationLinks I have.
I've ended up just adding a EmptyView() and it solved the problem.
I think this all stems from the original SwiftUI design decision that navigation link destinations are not lazy loaded. What a pain navigation has been from the beginning and getting more and more messier.
Once @basememara mentioned that is may have something to do with the lazy loading I immediately thought about some (but not all) of my NavigationLinks using the following wrapper:

Code Block Swift
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
NavigationLink(destination: NavigationLazyView(DetailView())) { Text("Click me") }

Adopted from this source.


So I tried wrapping every destination with the aforementioned wrapped and it seems that the navigation stopped popping views off the stack back and forth. The Unable to present. Please file a bug. is still there, but navigation seems to be working fine. Will post an update it the popping horror comes back.

EDIT: It still pops back and forth, but I think not so often as before. To be honest I already don't know if it is my fault or apple's that this works like that. In SwiftUI sometimes things just start working differently without any iOS version updates or anything like that. Feels like a live lab.
Another approach to the navigation link workaround collection is placing a hidden NavigationLink in the background that is shared across a few buttons, instead of each button being a NavigationLink:

Code Block swift
extension View {
    func navigate<Destination: View>(
        isActive: Binding<Bool>,
        destination: Destination?
    ) -> some View {
        background(
            NavigationLink(
                destination: destination,
                isActive: isActive,
                label: EmptyView.init
            )
            .hidden()
        )
    }
}
extension View {
    func navigate<Item, Destination: View>(
        item: Binding<Item?>,
        destination: (Item) -> Destination
    ) -> some View {
        navigate(
            isActive: Binding(
                get: { item.wrappedValue != nil },
                set: { if !$0 { item.wrappedValue = nil } }
            ),
            destination: Group {
                if let item = item.wrappedValue {
                    destination(item)
                }
            }
        )
    }
}

Then you can use it like this:

Code Block swift
struct ShowMoreView: View {
    @State private var isLinkActive = false
    var body: some View {
        List {
            Button(action: { isLinkActive = true })
                Text("Start navigation 1")
            }
            Button(action: { isLinkActive = true })
                Text("Start navigation 2")
            }
            Button(action: { isLinkActive = true })
                Text("Start navigation 3")
            }
        }
        .navigate(isActive: $isLinkActive, destination: makeDestination())
    }
}
struct ShowMoreView: View {
    @State private var date: Date?
    var body: some View {
        List {
            Button(action: { date = Date() })
                Text("Start navigation 1")
            }
            Button(action: { date = Date() + 100 })
                Text("Start navigation 2")
            }
            Button(action: { date = Date() + 1000 })
                Text("Start navigation 3")
            }
        }
        .navigate(item: $date, destination: makeDestination)
    }
    func makeDestination(for date: Date) -> some View {
        ...
    }
}


This source is from SwiftWithMajid's blog (for some reason the forums is not allowing me to link to it, but search for "swiftui majid lazy navigation" and you'll find it).
I'm having similar issues but in macOS! Updating to macOS 11.3 w/ Xcode 12.5 messed up my existing app which worked just fine in previous versions. How is it that Apple hasn't fixed this.

update: tried the EmptyView() and it doesn't help. Clicking the navigation link still leads to a "unable to present, please file a bug" error.

Frustrating.
Same issue here. Alas the workaround doesn't work in my case.

I am using a @State variable to store the isActive state and I had noticed it being false after I had set it to true during some renders of the body property. It renders the body with it true then all of a sudden it is false. Its almost like SwiftUI created a totally new View.

I tried putting the isActive state in my view model (I have an MVVM app) but I was able to watch it getting set to false through no action on my own (I guess when SwiftUI popped the newly pushed on view.) I also noticed my viewModel (passed via @EnviromentObject) was also nil during some of the body property calls.
I was able to resolve all navigation issues and state side effects in my app (iOS & iPadOS) using an adaptation of the method referenced by "basememara" and originating from "Swift with Majid". I'm mostly using grid and list based item views with navigation links.

SwiftUI is broken in so many areas currently, that the slowdown you get by finding many plan B's will eat all its proposed swiftness. I filed bug reports for all of them, please do the same.
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
Add a Comment
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?

  • The issue seems fixed for me as well with Xcode 13 beta 1.

Add a Comment

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()
        }
    }
}
Add a Comment