0

I don't understand why SwiftUI NavigationLink's isActive behaves as if it has it's own state. Even though I pass a constant to it, the back button overrides the value of the binding once pressed.

Code:


import Foundation
import SwiftUI

struct NavigationLinkPlayground: View {

    @State
    var active = true

    var body: some View {

        NavigationView {
            VStack {
                Text("Navigation Link playground")
                Button(action: { active.toggle() }) {
                    Text("Toggle")
                }

                Spacer()
                        .frame(height: 40)

                FixedNavigator(active: active)
            }
        }
    }

}

fileprivate struct FixedNavigator: View {

    var active: Bool = true

    var body: some View {
        return VStack {
            Text("Fixed navigator is active: \(active)" as String)

            NavigationLink(
                    destination: SecondScreen(),
                    // this is technically a constant!
                    isActive: Binding(
                            get: { active },
                            set: { newActive in print("User is setting to \(newActive), but we don't let them!") }
                    ),
                    label: { Text("Go to second screen") }
            )
        }
    }

}

fileprivate struct SecondScreen: View {

    var body: some View {
        Text("Nothing to see here")
    }

}


This is a minimum reproducible example, my actual intention is to handle the back button press manually. So when the set inside the Binding is called, I want to be able to decide when to actually proceed. (So like based on some validation or something.)

And I don't understand what is going in and why the back button is able to override a constant binding.

andras
  • 3,305
  • 5
  • 30
  • 45
  • 1
    NavigationLink is one-way directional, back button belongs to and managed by NavigationView, not by link. We don't have control over it (at least for now) - create custom back button if you want to manage it. – Asperi Apr 13 '21 at 07:37
  • I still cannot understand why the binding's constant value does not override if the link is active or not. I thought using a constant set to `true` would prevent back navigation altogether. – andras Apr 13 '21 at 07:58

1 Answers1

2

Your use of isActive is wrong. isActive takes a binding boolean and whenever you set that binding boolean to true, the navigation link gets activated and you are navigated to the destination.
isActive does not control whether the navigation link is clickable/disbaled or not. Here's an example of correct use of isActive. You can manually trigger the navigation to your second view by setting activateNavigationLink to true.

EDIT 1:

In this new sample code, you can disable and enable the back button at will as well:

struct ContentView: View {
    
    @State var activateNavigationLink = false
    
    var body: some View {
        NavigationView {
            VStack {
                // This isn't visible and should take 0 space from the screen!
                // Because its `label` is an `EmptyView`
                // It'll get programmatically triggered when you set `activateNavigationLink` to `true`.
                NavigationLink(
                    destination: SecondScreen(),
                    isActive: $activateNavigationLink,
                    label: EmptyView.init
                )
                
                Text("Fixed navigator is active: \(activateNavigationLink)" as String)
                
                Button("Go to second screen") {
                    activateNavigationLink = true
                }
            }
        }
    }
}

fileprivate struct SecondScreen: View {
    
    @State var backButtonActivated = false
    
    var body: some View {
        VStack {
            Text("Nothing to see here")
            
            Button("Back button is visible: \(backButtonActivated)" as String) {
                backButtonActivated.toggle()
            }
        }
        .navigationBarBackButtonHidden(!backButtonActivated)
    }
}
Mahdi BM
  • 1,826
  • 13
  • 16
  • My issue is that `isActive` does not control the current state of the `NavigationLink`, because it can be overridden once the back is pressed. I didn't mention the enabled / disabled part, I just wanted to show how it works. – andras Apr 13 '21 at 07:55
  • I know you can trigger the navigation, the unexpected thing to me was that you cannot prevent it. – andras Apr 13 '21 at 07:56
  • thanks for the explanation, let me see if i can find something to solve that. – Mahdi BM Apr 13 '21 at 07:57
  • does this answer your question or im still off? – Mahdi BM Apr 13 '21 at 08:01
  • Well, the back button should always be there, should register clicks but it should ultimately be me who decides to procees or not – andras Apr 13 '21 at 08:07
  • So if i understand correctly, you want the back button to be there and be clickable but not work unless you enable it to work? What would be the point of a back button that doesnt work?! im not sure if thats even doable using UIKit. you can workaround it though, for example showing a button with title "back button is disabled" when you hide the back button. – Mahdi BM Apr 13 '21 at 08:13
  • Imagine a scebario where an alert is presented to the user if they really want to go back or something like that – andras Apr 13 '21 at 08:16
  • 1
    you can change `backButtonActivated` to true at will to enable the back button again. Also you can self-dismiss the view using this method: https://www.hackingwithswift.com/quick-start/swiftui/how-to-make-a-view-dismiss-itself – Mahdi BM Apr 13 '21 at 08:19
  • 1
    Thanks, I think these will cover my cases – andras Apr 13 '21 at 08:21
  • I would't mind an upvote / answer acceptance if you saw my answer worthy and it solved your problem :) – Mahdi BM Apr 13 '21 at 09:09