Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dismiss a parent modal in SwiftUI from a NavigationView

Tags:

ios

swiftui

I am aware of how to dismiss a modal from a child view using @Environment (\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss() but this is a different issue.

When you present a multi-page NavigationView in a modal window, and have navigated through a couple of pages, the reference to presentationMode changes to be the NavigationView, so using self.presentationMode.wrappedValue.dismiss() simply pops the last NavigationView rather than dismissing the containing modal.

Is it possible - and if so how - to dismiss the containing modal from a page in a NavigationView tree?

Here's a simple example showing the problem. If you create an Xcode Single View app project using SwiftUI and replace the default ContentView code with this, it should work with no further changes.

import SwiftUI

struct ContentView: View {
  @State var showModal: Bool = false

  var body: some View {
    Button(action: {
      self.showModal.toggle()
    }) {
      Text("Launch Modal")
    }
    .sheet(isPresented: self.$showModal, onDismiss: {
      self.showModal = false
    }) {
      PageOneContent()
    }
  }
}

struct PageOneContent: View {
  var body: some View {
    NavigationView {
      VStack {
        Text("I am Page One")
      }
      .navigationBarTitle("Page One")
      .navigationBarItems(
        trailing: NavigationLink(destination: PageTwoContent()) {
          Text("Next")
        })
      }
  }
}

struct PageTwoContent: View {

  @Environment (\.presentationMode) var presentationMode

  var body: some View {
    NavigationView {
      VStack {
        Text("This should dismiss the modal. But it just pops the NavigationView")
          .padding()

        Button(action: {
          // How to dismiss parent modal here instead
          self.presentationMode.wrappedValue.dismiss()
        }) {
          Text("Finish")
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.blue)
      }
      .navigationBarTitle("Page Two")
    }
  }
}
like image 773
codewithfeeling Avatar asked Jan 20 '20 13:01

codewithfeeling


2 Answers

Here is possible approach based on usage own explicitly created environment key (actually I have feeling that it is not correct to use presentationMode for this use-case.. anyway).

Proposed approach is generic and works from any view in modal view hierarchy. Tested & works with Xcode 11.2 / iOS 13.2.

// define env key to store our modal mode values
struct ModalModeKey: EnvironmentKey {
    static let defaultValue = Binding<Bool>.constant(false) // < required
}

// define modalMode value
extension EnvironmentValues {
    var modalMode: Binding<Bool> {
        get {
            return self[ModalModeKey.self]
        }
        set {
            self[ModalModeKey.self] = newValue
        }
    }
}


struct ParentModalTest: View {
  @State var showModal: Bool = false

  var body: some View {
    Button(action: {
      self.showModal.toggle()
    }) {
      Text("Launch Modal")
    }
    .sheet(isPresented: self.$showModal, onDismiss: {
    }) {
      PageOneContent()
        .environment(\.modalMode, self.$showModal) // < bind modalMode
    }
  }
}

struct PageOneContent: View {
  var body: some View {
    NavigationView {
      VStack {
        Text("I am Page One")
      }
      .navigationBarTitle("Page One")
      .navigationBarItems(
        trailing: NavigationLink(destination: PageTwoContent()) {
          Text("Next")
        })
      }
  }
}

struct PageTwoContent: View {

  @Environment (\.modalMode) var modalMode // << extract modalMode

  var body: some View {
    NavigationView {
      VStack {
        Text("This should dismiss the modal. But it just pops the NavigationView")
          .padding()

        Button(action: {
          self.modalMode.wrappedValue = false // << close modal
        }) {
          Text("Finish")
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.blue)
      }
      .navigationBarTitle("Page Two")
    }
  }
}
like image 174
Asperi Avatar answered Oct 26 '22 02:10

Asperi


Another Approach would be to simply use a notification for this case and just reset the triggering flag for your modal. It is not the most beautiful solution for me but it is the solution I am most likely to still understand in a few months.

import SwiftUI

struct ContentView: View {
    @State var showModalNav: Bool = false
    
    var body: some View {
        Text("Present Modal")
            .padding()
            .onTapGesture {
                showModalNav.toggle()
            }.sheet(isPresented: $showModalNav, content: {
                ModalNavView()
            }).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
                showModalNav = false
            }
    }
}


struct ModalNavView: View {
    var body: some View {
        NavigationView {
            NavigationLink(
                destination: PushedView(),
                label: {
                    Text("Show Another View")
                }
            )
        }
    }
}

struct PushedView: View {
    var body: some View {
        Text("Pushed View").onTapGesture {
            NotificationCenter.default.post(Notification.init(name: Notification.Name(rawValue: "PushedViewNotifciation")))
        }
    }
}

If you don't want to loosely couple the views through a notification you could also just use a binding for this like so:

struct ContentView: View {
    @State var showModalNav: Bool = false
    
    var body: some View {
        Text("Present Modal")
            .padding()
            .onTapGesture {
                showModalNav.toggle()
            }.sheet(isPresented: $showModalNav, content: {
                ModalNavView(parentShowModal: $showModalNav)
            }).onReceive(NotificationCenter.default.publisher(for: Notification.Name(rawValue: "PushedViewNotifciation"))) { _ in
                showModalNav = false
            }
    }
}


struct ModalNavView: View {
    @Binding var parentShowModal: Bool
    var body: some View {
        NavigationView {
            NavigationLink(
                destination: PushedView(parentShowModal: $parentShowModal),
                label: {
                    Text("Show Another View")
                }
            )
        }
    }
}

struct PushedView: View {
    @Binding var parentShowModal: Bool
    var body: some View {
        Text("Pushed View").onTapGesture {
            parentShowModal = false
        }
    }
}
like image 37
Sebastian Boldt Avatar answered Oct 26 '22 02:10

Sebastian Boldt