在SwiftUI中从NavigationView中关闭父级模态视图

16

我知道如何使用@Environment(\.presentationMode) var presentationMode / self.presentationMode.wrappedValue.dismiss()来关闭子视图中的模态窗口,但这是一个不同的问题。

当您在模态窗口中呈现多页NavigationView并浏览了几页后,对presentationMode的引用会更改为NavigationView,因此使用self.presentationMode.wrappedValue.dismiss()仅会弹出最后一个NavigationView而不是关闭包含的模态窗口。

是否有可能 - 如果可能的话,如何 - 从NavigationView树中的页面关闭包含的模态窗口?

以下是一个简单的示例,显示了此问题。如果您使用SwiftUI创建Xcode单个视图应用程序项目并将默认的ContentView代码替换为此代码,则无需进行其他更改即可正常工作。

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")
    }
  }
}

我学习SwiftUI已经有几周了,所以我不确定,但是你是否尝试将“trailing: NavigationLink(destination: PageTwoContent()) {”替换为“trailing: PageTwoContent() {”,或者删除PageTwoContent内部的NavigationView块。 - Simone Pistecchia
@SimonePistecchia,恐怕你的建议都不太合适,但还是谢谢。 - codewithfeeling
有趣…我在你的快照 UIKit-3900.12.15/UINavigationController.m:8129 中遇到了异常错误:CRASH: Tried to pop to a view controller that doesn't exist. 你也是吗? - Asperi
@Asperi - 不,它运行得很好。虽然我刚刚进行了更改,使其可以与默认的Xcode SwiftUI项目开箱即用。 - codewithfeeling
4个回答

15

这里是一种可能的方法,基于使用自己明确创建的环境键(实际上我有一种感觉,对于这种用例使用presentationMode不正确...无论如何)。

建议的方法是通用的,并且适用于模态视图层次结构中的任何视图。经过测试,在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")
    }
  }
}

谢谢 - 我可以确认这个方法完美地运行。我只是想等待看看是否有其他方法或对此方法的评论,但假设没有缺点,我很快会将其标记为正确答案。 - codewithfeeling

6

另一种方法是仅针对此情况使用通知,并重置模态框的触发标志。

这对我来说不是最美观的解决方案,但这是我在几个月内最可能理解的解决方案。

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")))
        }
    }
}

如果您不想通过通知来松散地耦合视图,您也可以像这样使用绑定进行:

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
        }
    }
}

1
如果只有两个级别,特别是在多个级别上可以解除表格的情况下,您可以将showModal包含为导航视图中的绑定变量,然后在任何地方切换它都会解除整个表格。
我认为您可以像Wei上面提到的那样使用showModal作为EnvironmentObject来做类似的事情 - 如果有超过两个级别并且您只想在最具体的级别上解除表格,这可能更好。
我记不清是否有某些原因要远离将其作为绑定变量进行操作,但对我来说似乎正在工作。
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
    }) {
      // Bind showModal to the corresponding property in PageOneContent
      PageOneContent(showModal: $showModal)
    }
  }
}

然后您将showModal作为绑定变量添加到PageOneContent中,并将其绑定到ContentView中的状态变量。
struct PageOneContent: View {
  // add a binding showModal var in here
  @Binding var showModal: Bool

  var body: some View {
    NavigationView {
      VStack {
        Text("I am Page One")
      }
      .navigationBarTitle("Page One")
      .navigationBarItems(
        // bind showModal again to PageTwoContent
        trailing: NavigationLink(destination: PageTwoContent(showModal: $showModal)) {
          Text("Next")
        })
      }
  }
}

最后,在PageTwoContent中,您可以在此处添加showModal(并且在PageOneContent中的NavigationLink中,已将PageTwoContent的showModal绑定到PageOneContent)。然后在您的按钮中,您所需要做的就是切换它,它将关闭该表。

struct PageTwoContent: View {

  // Add showModal as a binding var here too.
  @Binding var showModal: Bool

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

        Button(action: {
          // This will set the showModal var back to false in all three views, and will dismiss the current sheet.
          self.showModal.toggle()
        }) {
          Text("Finish")
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.blue)
      }
      .navigationBarTitle("Page Two")
    }
  }
}

-3

我发现你实际上可以将showModal转换为EnvironmentObject,然后简化在PageTwoContent中切换showModal为false,以关闭PageOneContent和PageTwoContent。


详细说明!或者留下评论,而不是回答。 - drago

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接