SwiftUI NavigationLink pops automatically which is unexpected

4.6k Views Asked by At

I am having some issues with a NavigationLink on an iPad with split view (landscape). Here is an example:

screen recording

Here is the code to reproduce the issue:

import SwiftUI

final class MyEnvironmentObject: ObservableObject {
    @Published var isOn: Bool = false
}

struct ContentView: View {
    @EnvironmentObject var object: MyEnvironmentObject

    var body: some View {
        NavigationView {
            NavigationLink("Go to FirstDestinationView", destination: FirstDestinationView(isOn: $object.isOn))
        }
    }
}

struct FirstDestinationView: View {
    @Binding var isOn: Bool

    var body: some View {
        NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView(isOn: $isOn))
    }
}

struct SecondDestinationView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle(isOn: $isOn) {
            Text("Toggle")
        }
    }
}

// Somewhere in SceneDelegate
ContentView().environmentObject(MyEnvironmentObject())

Does anyone know a way to fix this? An easy fix is to disable split view, but that is not possible for me.

3

There are 3 best solutions below

0
On

You need can use isDetailLink(_:) to fix that, e.g.

struct FirstDestinationView: View {
    @Binding var isOn: Bool

    var body: some View {
        NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView(isOn: $isOn))
        .isDetailLink(false)
    }
}
0
On

When something within EnvironmentObject changes, it will render the whole view again including NavigationLink. That's the root cause of automatic pop back.

My research on it:

  • OK on iOS 15 (seems Apple fixed)
  • Still broken on iOS 14
  • The reason why "This bug went away when I dropped the @EnvironmentObject and went with an @ObservedObject instead." @Jon Vogel mentioned is ObservedObject is a local state, which will not be affected by other views while EnvironmentObject is global state and can change from any other remote views.
2
On

Ok, here is my investigation results (tested with Xcode 11.2) and below is the code that works.

In iPad NavigationView got into Master/Details style, so ContentView having initial link is active and process bindings update from environmentObject, so refresh, which result in activating link of details view via same binding, thus corrupting navigation stack. (Note: this is absent in iPhone due to stack style, which deactivates root view).

So, probably this is workaround, but works - the idea is not to pass binding from view to view, but use environmentObject directly in final view. Probably this will not always be a case, but anyway in such scenarios it is needed to avoid root view refresh, so it should not have same binding in body. Something like that.

final class MyEnvironmentObject: ObservableObject {
    @Published var selection: Int? = nil
    @Published var isOn: Bool = false
}

struct ContentView: View {
    @EnvironmentObject var object: MyEnvironmentObject

    var body: some View {
        NavigationView {
            List {
                NavigationLink("Go to FirstDestinationView", destination: FirstDestinationView())
            }
        }
    }
}

struct FirstDestinationView: View {

    var body: some View {
        List {
            NavigationLink("Go to SecondDestinationView", destination: SecondDestinationView())
        }
    }
}

struct SecondDestinationView: View {
@EnvironmentObject var object: MyEnvironmentObject

    var body: some View {
        VStack {
            Toggle(isOn: $object.isOn) {
                Text("Toggle")
            }
        }
    }
}