JAHelia
JAHelia

Reputation: 7932

SwiftUI: detecting the NavigationView back button press

In SwiftUI I couldn't find a way to detect when the user taps on the default back button of the navigation view when I am inside DetailView1 in this code:

struct RootView: View {
    @State private var showDetails: Bool = false
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView1(), isActive: $showDetails) {
                    Text("show DetailView1")
                }
            }
            .navigationBarTitle("RootView")
        }
    }
}

struct DetailView1: View {
    @State private var showDetails: Bool = false
    var body: some View {
        NavigationLink(destination: DetailView2(), isActive: $showDetails) {
            Text("show DetailView2")
        }
        .navigationBarTitle("DetailView1")
    }
}

struct DetailView2: View {
    var body: some View {
        Text("")
            .navigationBarTitle("DetailView2")
    }
}

Using .onDisappear doesn't solve the problem as its closure is called when the view is popped off or a new view is pushed.

Upvotes: 30

Views: 20964

Answers (7)

Byron
Byron

Reputation: 51

It is possible to use .onDisappear to detect when a user has tapped the default back button.

the .onDisappear closure is called when the view is popped off or a new view is pushed.

However when a new View has been pushed the value of the calling Views State variable will be set to true. When the user taps the Back button value of calling Views State variable will be set to false.

So with the attached code:

Tapping DetailView1

Tapping DetailView2

Tapping DetailView2 Back Button

Tapping DetailView1 Back Button

will produce the following output:

onDisappear DetailView1; showDetails true

onDisappear DetailView1; A new view has been pushed

onDisappear DetailView2; showDetails true

onDisappear DetailView2; A new view has been pushed

onDisappear DetailView2; showDetails false

onDisappear DetailView2; Back button has been pressed: Could do something here

onDisappear DetailView1; showDetails false

onDisappear DetailView1; Back button has been pressed: Could do something here

struct RootView: View {
@State private var showDetails: Bool = false
var body: some View {
    NavigationView {
        VStack {
            NavigationLink(destination: DetailView1(), isActive: $showDetails) {
                Text("show DetailView1")
                    .onDisappear { // closure is called when the view is popped off or a new view is pushed
                        print("onDisappear DetailView1; showDetails \(showDetails)")
                        // showDetails will be set to:
                        // - true if a new view has been pushed
                        // - false if user has tapped the default back button
                        if !showDetails {
                            print("onDisappear DetailView1; Back button has been pressed: Could do something here")
                        } else {
                            print("onDisappear DetailView1; A new view has been pushed")
                        }
                    }
            }
        }
        .navigationBarTitle("RootView")
    }
}

}

struct DetailView1: View {
@State private var showDetails: Bool = false
var body: some View {
    NavigationLink(destination: DetailView2(), isActive: $showDetails) {
        Text("show DetailView2")
            .onDisappear { // closure is called when the view is popped off or a new view is pushed
                print("onDisappear DetailView2; showDetails \(showDetails)")
                // showDetails will be set to:
                // - true if a new view has been pushed
                // - false if user has tapped the default back button
                if !showDetails {
                    print("onDisappear DetailView2; Back button has been pressed: Could do something here")
                } else {
                    print("onDisappear DetailView2; A new view has been pushed")
                }
            }
    }
    .navigationBarTitle("DetailView1")
}

}

struct DetailView2: View {
var body: some View {
    Text("")
        .navigationBarTitle("DetailView2")
}

}

Upvotes: 1

NFC.cool
NFC.cool

Reputation: 3303

When working with NavigationPath you can do something like that:

struct BackDetectionModifier: ViewModifier {

    @EnvironmentObject var navigationModel: NavigationModel
    @State private var currentPathCount = 0
    var closure: () -> Void

    func body(content: Content) -> some View {
        content
            .onAppear {
                currentPathCount = navigationModel.navigationPath.count
            }
            .onDisappear {
                if currentPathCount > navigationModel.navigationPath.count {
                    // Back button was tapped
                    closure()
                }
            }
    }
}

extension View {
    func onBackButtonTapped(_ closure: @escaping () -> Void) -> some View {
        self.modifier(BackDetectionModifier(closure: closure))
    }
}

Upvotes: 1

Barrrdi
Barrrdi

Reputation: 1161

@93sauu’s approach is the way. But a commenter correctly pointed out that this will result in swipe to back to not function. The correction to this is to drop this extension:

extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }
    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

On iOS 17 at least, this worked with no issues.

Upvotes: 2

macrael
macrael

Reputation: 346

As soon as you press the back button, the view sets isPresented to false, so you can use an observer on that value to trigger code when the back button is pressed. Assume this view is presented inside a navigation controller:

struct MyView: View {
    @Environment(\.isPresented) var isPresented

    var body: some View {
        Rectangle().onChange(of: isPresented) { newValue in
            if !newValue {
                print("detail view is dismissed")
            }
        }
    }
}
    

Upvotes: 13

markiv
markiv

Reputation: 1664

An even nicer (SwiftUI-ier?) way of observing the published showDetails property:

struct RootView: View {
    class ViewModel: ObservableObject {
        @Published var showDetails = false
    }
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
                    Text("show DetailView1")
                }
            }
            .navigationBarTitle("RootView")
            .onReceive(self.viewModel.$showDetails) { isShowing in
                debugPrint(isShowing)
                // Maybe do something here?
            }
        }
    }
}

Upvotes: 2

markiv
markiv

Reputation: 1664

Following up on my comment, I would react to changes in the state of showDetails. Unfortunately didSet doesn't appear to trigger with @State variables. Instead, we can use an observable view model to hold the state, which does allow us to do intercept changes with didSet.

struct RootView: View {
    class ViewModel: ObservableObject {
        @Published var showDetails = false {
            didSet {
                debugPrint(showDetails)
                // Maybe do something here?
            }
        }
    }
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DetailView1(), isActive: $viewModel.showDetails) {
                    Text("show DetailView1")
                }
            }
            .navigationBarTitle("RootView")
        }
    }
}

Upvotes: 1

93sauu
93sauu

Reputation: 4107

The quick solution is to create a custom back button because right now the framework have not this possibility.

struct DetailView : View {

    @Environment(\.presentationMode) var mode: Binding<PresentationMode>

    var body : some View {
        Text("Detail View")
            .navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: Button(action : {
                self.mode.wrappedValue.dismiss()
            }){
                Image(systemName: "arrow.left")
            })
    }
}

Upvotes: 28

Related Questions