Reputation: 7932
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
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
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
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
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
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
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
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