14.5 beta3 NavigationLink unexpected pop

Interesting, thanks for the feedback on that. I refactored the example to use discrete state for each screen and you're right, I don't see the pop happening. I'll try to retrofit this into my project to see if it fixes my issue

enum MyAction {
    case setSecondScreenVisible(Bool)
    case setThirdScreenVisible(Bool)
}

struct MyState: Equatable {
    var isSecondScreenVisible: Bool = false
    var isThirdScreenVisible: Bool = false
}

struct Environment {}

let myReducer = Reducer<MyState, MyAction, Environment> { state, action, environment in
    switch action {

    case .setSecondScreenVisible(let isVisible):
        state.isSecondScreenVisible = isVisible
        return .none

    case .setThirdScreenVisible(let isVisible):
        state.isThirdScreenVisible = isVisible
        return .none
    }
}

struct FirstScreen: View {
    let store: Store<MyState, MyAction>

    public var body: some View {
        WithViewStore(self.store) { viewStore in
            NavigationView {
                VStack {
                    Button("Go to screen 2") {
                        viewStore.send(.setSecondScreenVisible(true))
                    }

                    NavigationLink(
                        destination: SecondScreen(store: store),
                        isActive: viewStore.binding(get: \.isSecondScreenVisible, send: MyAction.setSecondScreenVisible(_:)),
                        label: {
                            EmptyView()
                        }
                    )
                }
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
    }
}

struct SecondScreen: View {
    let store: Store<MyState, MyAction>

    public var body: some View {
        WithViewStore(self.store) { viewStore in
            VStack {
                Button("Go to screen 3") {
                    viewStore.send(.setThirdScreenVisible(true))
                }

                NavigationLink(
                    destination: Text(verbatim: "Third Screen"),
                    isActive: viewStore.binding(get: \.isThirdScreenVisible, send: MyAction.setThirdScreenVisible(_:)),
                    label: {
                        EmptyView()
                    }
                )
            }
        }
    }
}

1 Like

A quick follow up as I've totally solved the problem on my project:

in my case I had 3 onboarding screens, all sharing OnboardingState, OnboardingActions. A navigation stack might look like

WelcomeScreen > LoginScreen > TwoFactorScreen

When on the TwoFactorScreen, any change to the onboarding state would cause a re-render in both the Login and Welcome Screens. During this re-render, the NavigationLinks would be either nilled, or read with the incorrect value (still not entirely sure which). This would lead to the TwoFactorScreen to be popped from the navigation stack and I would be taken back to the login screen.

To solve the issue I had to split any state in each of the screens to ensure that nothing was being shared, ie

    public var isLoginVisible: Bool = false
    public var isTwoFactorVisible: Bool = false

    public var isLoginRequestInProgress: Bool = false
    public var isTwoFactorRequestInProgress: Bool = false

And I also needed to scope my viewStores to only the state that the individual Screen cared about, ie

    let store: Store<OnboardingState, OnboardingAction>

    @ObservedObject var viewStore: ViewStore<ViewState, OnboardingAction>

    struct ViewState: Equatable {
        var email: String
        var password: String
        var isTwoFactorVisible: Bool
        var requestInProgress: Bool

        init(state: OnboardingState) {
            email = state.email
            password = state.password
            isTwoFactorVisible = state.isTwoFactorVisible
            requestInProgress = state.isLoginRequestInProgress
        }
    }

    public init(store: Store<OnboardingState, OnboardingAction>) {
        self.store = store
        self.viewStore = ViewStore(self.store.scope(state: ViewState.init))
    }

Hope that helps others in this thread. Not sure why this just started coming up in iOS 14.5, but this seems to be the solution

Glad you sorted it - some advice from somebody who has literally just implemented a near identical flow (Landing -> Login -> Maybe TFA) - it can be helpful to divide up your state into discrete domains, so your initial root state might be:

struct AppState: Equatable {
  var loginState: LoginState?
}

enum AppAction {
  case tappedStartLoginButton // reducer will initialise `LoginState`
}

You can use the presence of the login state to drive the navigation to the login screen.

Then you might have:

struct LoginState: Equatable {
  var twoFactorAuthState: TwoFactorAuthState?
}

Breaking your state up in this way makes it much easier to reason about discrete parts of your state, especially through a navigation hierarchy like this and will help avoid over-rendering your views.

In fact there's a good example of this in the TCA repo:

1 Like

Yep. The view with the links gets recalculated and that causes the pop. Nothing that affects the NavigationLinks is getting touched though. It's the re-rendering itself that's triggering it, for sure.

Using vanilla SwiftUI in WatchOS 7.4 (the new release) and the problem exists there as well. Same workaround fixes it.

1 Like

Here's my extremely simple view—no state at all—which illustrates the problem. I would think I'm mis-using the API, but XCode is helpfullying saying, at run-time "Unable to present. Please file a bug." This began yesterday with Apple's release of XCode 12.5/etc. This uses the "split-screen" feature of NavigationView (supplying two views). Note that the problem does not exist when .navigationViewStyle(StackNavigationViewStyle()) is added (this changes the navigation in a way that I don't want, so is not a "solution"). Thanks for reviewing, all!

import SwiftUI

struct ContentView: View {
  var body: some View {
    NavigationView {
      Group {

        NavigationLink(destination: Text("Destination 1")) {
          Text("Hello, 1!")
        }
        NavigationLink(destination: Text("Destination 2")) {
          Text("Hello, 2!")
        }
        NavigationLink(destination: Text("Destination 3")) {
          Text("Hello, 3!")
        }
        
      }
      
      Group {
        Text("Secondary")
      }
    }

  }
}
1 Like

I’m running into this issue too and it also affects code compiled with the 14.4 SDK running on iPadOS 15.5.
The only workarounds I’ve found are embedding the NavigationLinks in a List or Form, though both apply a number of style and behaviour changes to their children which prevents me from using this approach in production.

Strangly the issue also seems to disappear if you have fewer than three NavigationLinks.

Interesting (for me at least) it seems the first time a link is triggered the destination view is presented correctly, but subsequent triggers fail.

Feel free to dupe my feedback/radar FB9091466, if you already have one let me know and I’ll reference it in mine.

I've also uploaded my sample project here: navvy/ContentView.swift at master · tomhut/navvy · GitHub

2 Likes

I can confirm this issue. My app was working perfectly fine before the recent update. Now navigation is completely broken, screens stay white or multiple screens overlay each other with a semi-transparent background.

I have a device which still runs iOS 14.4, but the problem also occurs on it. So I assume that XCode 12.5 has some changes which cause this issue and not iOS itself.

1 Like

I have exactly the same issue, started occurring with Xcode 12.5 / iOS 14.5. Only on iPadOS, not using StackNavigationViewStyle (I want two columns) and with 5 NavigationLinks in the same view.

Interesting (for me at least) it seems the first time a link is triggered the destination view is presented correctly, but subsequent triggers fail.

This happens for me as well. I hope this can be fixed relatively quickly.

1 Like

This may be related to this note from the developer release notes:

  • The destination of NavigationLink that only differs by local state now resets that state when switching between links as expected. (72117345)

Now, what that actually means I have no idea.

Make sure to report the issue to Apple using Feedback if you want any chance to see it fixed. (Feedback currently seems to be down.)

Seeing the same issue on a fairly small application, for me it was happening with a similar setup as OP.

Exactly two navigation links on a screen using two distinct elements of the state.

Temp fix by adding a hidden NavigationLink :\ - feedback submitted.

Hey folks, I was able to reliably reproduce the issue without using TCA aka vanilla SwiftUI (I've update my repo).
I narrowed it down to 2 main cases (so far):

  1. Updating an @State property only when that property is referenced anywhere in the view hierarchy.
  2. Updating an @ObservedObject / @StateObject property even when it is not referenced in the view hierarchy.

Ensuring the View does not have exactly 2 NavigationLinks works around the issue in both cases.

We are seeing this behavior quite frequently using TCA because of the design of composing states and its use of @ObservedObject for the ViewStore.

Has there been any fix to this?

My macOS app now keeps showing "Unable to present" when clicking on a NavigationLink and the child view never opens. The app is essentially unusable after the Big Sur 11.3 / Xcode 12.5 update.

Surely many others are impacted?

I tried the EmptyView() fix to no avail.

I also had exactly this problem.

Above gist is a simple code for my problem situation.

And the gist below is the code of the way I solved the problem right away.

I don't know why this issue happen. but still occurred in iOS 14.5, iOS 14.6

If you have better solution, please let me know.

I ran you code and there is no problem. Xcode Version 12.5 (12E262)

Your used of Groups doesn't make sense. The Text("Secondary") is not shown on screen. All should be just inside one VStack

@young Thanks for running my code sample. Are you running it in an iPad where both views of the NavigationView (primary and secondary) are in effect and displayed? If on the iPhone it works fine—the bug appears when the Secondary is displayed because, as I mentioned, "This uses the split-screen feature of NavigationView (supplying two views)." I agree a VStack is better but it's beside the point and changing to a VStack does not fix the problem. I just re-ran it in XCode 12.5 (12E262). If you try it in iPad, please let me know if the bug exhibits or not.

I was running in. iPhone. In iPad does show problem:

So it shows blank screen with "< Back" button on start up and NavLink only navigate once, after none work. Don't see "Unable to present. Please file a bug."

1 Like

Fixed in XCode 13 Beta / iPadOS 15 simulator! My code sample from April 28 (above: using NavigationLink in side-by-side 2-panel iPad view, with no state at all), which was broken in XCode 12 with iOS 14.5 now works well in XCode 13 Beta (running in the iPadOS 15 simulator). Good enough for me!

2 Likes

Thanks man I had the same issue with that on none beta version
it fixed my issue of unexpected pop on @EnvironmentObject value change.

i think the bug is still in it.
but with @EnvironmentObject

my screen transition was
Scanner to confirmation to login to home to login to scanner to confirmation to login to home and in home I tried to change @EnvironmentObject and it poped me to Scanner screen instead just new pop up

In case folks were looking for more concrete examples of how to fix the problem in TCA, a discussion was opened on GitHub recently: Multiple NavigationLink with composable architecture · Discussion #670 · pointfreeco/swift-composable-architecture · GitHub

Terms of Service

Privacy Policy

Cookie Policy