【问题标题】:SwiftUI dismiss modalSwiftUI 关闭模式
【发布时间】:2019-10-24 07:45:48
【问题描述】:

由于 SwiftUI 是声明式的,因此没有 dismiss 方法。 如何向DetailView 添加关闭/关闭按钮?

struct DetailView: View {
  var body: some View {
  Text("Detail")
  }
}

struct ContentView : View {
  var body: some View {
  PresentationButton(Text("Click to show"), destination: DetailView())
  }
}

【问题讨论】:

  • 我所见过的示例都没有关闭呈现视图的方法,所以我认为还没有。
  • 我很确定他们会在下一个 beta 版本中引入它。 Pop 方法也不见了。
  • 我认为记住 SwiftUI 是一种范式转变很重要。我们必须更多地考虑“状态”,而不是写出条件语句等。因此,正如其他人所写的,更多的是通过@Environment@State 或其他“属性包装器”来监听状态。 "对于那些喜欢复杂短语的人来说,这是向声明式框架中的观察者模式的转变:-)
  • 现在在 Beta 5 中有一种非常简洁的方法来执行此操作。请参阅下面的答案。顺便说一句,同样的方法适用于弹出导航视图。
  • 看起来他们在 iOS 15 中引入了你想要的——DismissAction。见this answer

标签: swift modal-dialog swiftui


【解决方案1】:

使用@State 属性包装器(推荐)

struct ContentView: View {
    @State private var showModal = false
    
    var body: some View {
       Button("Show Modal") {
          self.showModal.toggle()
       }.sheet(isPresented: $showModal) {
            ModalView(showModal: self.$showModal)
        }
    }
}

struct ModalView: View {
    @Binding var showModal: Bool
    
    var body: some View {
        Text("Modal view")
        Button("Dismiss") {
            self.showModal.toggle()
        }
    }
}

使用演示模式

您可以在模态视图中使用presentationMode 环境变量并调用self.presentaionMode.wrappedValue.dismiss() 来关闭模态:

struct ContentView: View {

  @State private var showModal = false

  // If you are getting the "can only present once" issue, add this here.
  // Fixes the problem, but not sure why; feel free to edit/explain below.
  @SwiftUI.Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>


  var body: some View {
    Button(action: {
        self.showModal = true
    }) {
        Text("Show modal")
    }.sheet(isPresented: self.$showModal) {
        ModalView()
    }
  }
}


struct ModalView: View {

  @Environment(\.presentationMode) private var presentationMode

  var body: some View {
    Group {
      Text("Modal view")
      Button(action: {
         self.presentationMode.wrappedValue.dismiss()
      }) {
        Text("Dismiss")
      }
    }
  }
}

【讨论】:

  • 如果使用 List 问题,我也经历过 Beta 3“只出现一次”。但是,在某些情况下,Beta 4 似乎破坏了 Modal 使用 isPresented 环境变量自行关闭的能力。上面的示例仍然有效,但我的示例无效。我仍在尝试隔离问题。
  • 我在Xcode Version 11.0 (11A419c) 中注意到,当使用self.presentationMode.wrappedValue.dismiss() 被调用时,.sheet( 上的onDismiss 函数没有被调用。当我通过下拉关闭模式视图时,回调被调用。
  • 你也可以只使用@Environment(\.presentationMode) var presentationMode,因为 Swift 会通过指定的 keypath 推断类型。
  • 这是错误的。您应该传递一个也用于 isPresented 的状态变量,而不是弄乱presentationMode。
  • 我同意@stardust4891。您应该传递一个状态变量。使用下面的答案。这可能会导致后期出现问题。例如。与 TabView 一起使用。
【解决方案2】:

在 Xcode Beta 5 中,另一种方法是在启动模式的视图中使用 @State,并在模式视图中添加绑定以控制模式的可见性。这不需要您访问@Environment presentationMode 变量。

struct MyView : View {
    @State var modalIsPresented = false

    var body: some View {
        Button(action: {self.modalIsPresented = true})  {
            Text("Launch modal view")
        }
        .sheet(isPresented: $modalIsPresented, content: {
            MyModalView(isPresented: self.$modalIsPresented)
        })
    }
}


struct MyModalView : View {
    @Binding var isPresented: Bool

    var body: some View {
        Button(action: {self.isPresented = false})  {
            Text("Close modal view")
        }
    }
}

【讨论】:

  • 感谢使用声明式方法和单一事实来源遵守 SwiftUI 的原则
  • 它只在第一次工作,如果我关闭并再次尝试打开窗口它就不再工作了。
  • 对我来说似乎没问题,也许您正在其他地方更改 isPresented 值?例如,如果您通过下拉关闭模式,swiftUI 会自动切换该值。不要将值显式设置为 true/false,而是尝试使用 isPresented.toggle() 来代替
  • 我同意@stardust4891 的观点,可惜演示模式的答案获得了更多的支持。当您查看有关 WrappedValue 的官方文档时,Apple 写道:“此属性提供对值数据的主要访问。但是,您不能直接访问 WrappedValue。相反,您使用使用 \@Binding 创建的属性变量属性。”如本答案所示,它刺激了绑定的使用。为了单一的事实来源。
  • 编写自己的@Binding var isPresented: Bool 是最灵活的选择。它甚至可以在你的虚拟机中声明为@Published,而不是上面的@State。我正在展示一个带有 NavigationView 的模式,我希望能够在任何 NavigationLink 的“完成按钮”上关闭整个模式。使用presentationMode,导致我不得不跟踪不必要的状态。但是只需绑定到我的虚拟机,我就可以通过简单地将isPresented 翻转为 false 来轻松地从任何完成按钮关闭模式。
【解决方案3】:

这是一种关闭呈现视图的方法。

struct DetailView: View {
    @Binding
    var dismissFlag: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.dismissFlag.toggle()
            }) {
                Text("Dismiss")
            }
        }

    }
}

struct ContentView : View {
    @State var dismissFlag = false

    var body: some View {
        Button(action: {
            self.dismissFlag.toggle()
        })
        { Text("Show") }
            .presentation(!dismissFlag ? nil :
                Modal(DetailView(dismissFlag: $dismissFlag)) {
                print("dismissed")
            })
    }
}

【讨论】:

  • 谢谢,但是如果用户拖动关闭,切换需要按两次。可以通过切换状态self.dismissFlag = true; self.dismissFlag = false; 来解决。解决方法,而不是解决方案。也在寻找一种禁用拖动以关闭的方法。
  • 我认为如果您在 Modal 构造函数中实现 onDismiss,您将能够使 dismissFlag 保持同步。我还没试过确定。
  • 为了验证这一点,我刚刚测试了self.dismissFlag 在使用拖动动作关闭视图时会发生什么。将 onDismiss: { print(self.dismissFlag) } 添加到您的 .sheet 以测试自己。拖动时似乎会自动切换变量。请注意,onDismiss 函数似乎仅在将模态视图拖走时才被调用。如果您通过自己切换self.dismissFlag 来关闭模式,则不会调用onDismiss。 (我使用的是 iOS 13 Beta 8)
【解决方案4】:

iOS 15

我们现在可以使用DismissAction,而不是presentationMode

这是来自documentation 的示例:

struct SheetView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            SheetContents()
                .toolbar {
                    Button("Done") {
                        dismiss()
                    }
                }
        }
    }
}

【讨论】:

  • 这对于 iOS 15 来说是一种简洁明了的方式。不过,我认为 - 由于大多数答案都提供了使用 @State@Environment 的解决方案,恕我直言,这不是 正确的 i> 在大多数用例中的方式。当模态显示到视图中时,这种方法会改变逻辑。观点中的逻辑?恕我直言,更好的方法是利用“视图模型”或类似的东西来执行逻辑。如果是模式,它只是提供一个适当的“视图状态”,它清楚地定义了何时显示模式,何时不显示,并且还处理“关闭”操作(由用户发起的函数调用)而不是视图
【解决方案5】:

似乎对于 Xcode 11 Beta 7(这是在 Xcode 的 build 11M392r 上)略有不同。

@Environment(\.presentationMode) var presentation


Button(action: { self.presentation.wrappedValue.dismiss() }) { Text("Dismiss") }

【讨论】:

  • 这是错误的。您应该传递一个也用于 isPresented 的状态变量,而不是弄乱presentationMode。
【解决方案6】:

你可以实现这个。

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

    private func dismiss() {
        isPresented?.value = false
    }
}

【讨论】:

  • 感谢您对环境的提示。如何在我的示例中访问外部的isPresented
  • 这是错误的。您应该传递一个也用于 isPresented 的状态变量,而不是弄乱presentationMode。
【解决方案7】:

现在在 Beta 5 中有一种非常简洁的方法。

import SwiftUI

struct ModalView : View {
    // In Xcode 11 beta 5, 'isPresented' is deprecated use 'presentationMode' instead
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        Group {
            Text("Modal view")
            Button(action: { self.presentationMode.wrappedValue.dismiss() }) { Text("Dismiss") }
        }
    }
}

struct ContentView : View {
    @State var showModal: Bool = false
    var body: some View {
        Group {
            Button(action: { self.showModal = true }) { Text("Show modal via .sheet modifier") }
                .sheet(isPresented: $showModal, onDismiss: { print("In DetailView onDismiss.") }) { ModalView() }
        }
    }
}

【讨论】:

  • 这是错误的。您应该传递一个也用于 isPresented 的状态变量,而不是弄乱presentationMode。
  • 在什么情况下出错了?您的问题是正确性问题还是风格偏好?还有多种其他方法可以完成同样的任务。 Apple 自己的 iOS 13 发行说明将此记录为消除 Modals 的一种方法,并且它有效。谢谢。
  • 这是一个有趣的见解,但是,它可能是也可能不是真正的问题。早些时候解释说,dismiss() 是为了方便而添加的,因此除了工作表修饰符之外,不必将与 isPresented var 的绑定传递给模态视图。它所做的只是将 isPresented var 设置为 false,如果它为 true,否则(根据 SwiftUI 头文件)它什么也不做。
  • 我认为传递@State 变量比使用PresentationMode 更好。 PresentationMode不会总是关闭模式。例如,如果您的模态 like in this answer 中有一个 NavigationView,那么调用 dismiss() 只会在您导航到其他屏幕时弹出到上一个视图。
【解决方案8】:

由于PresentationButton 易于使用,但隐藏状态会破坏SwiftUI 的预测特性,因此我使用可访问的Binding 实现了它。

public struct BindedPresentationButton<Label, Destination>: View where Label: View, Destination: View {
    /// The state of the modal presentation, either `visibile` or `off`.
    private var showModal: Binding<Bool>

    /// A `View` to use as the label of the button.
    public var label: Label

    /// A `View` to present.
    public var destination: Destination

    /// A closure to be invoked when the button is tapped.
    public var onTrigger: (() -> Void)?

    public init(
        showModal: Binding<Bool>,
        label: Label,
        destination: Destination,
        onTrigger: (() -> Void)? = nil
    ) {
        self.showModal = showModal
        self.label = label
        self.destination = destination
        self.onTrigger = onTrigger
    }

    public var body: some View {
        Button(action: toggleModal) {
            label
        }
        .presentation(
            !showModal.value ? nil :
                Modal(
                    destination, onDismiss: {
                        self.toggleModal()
                    }
                )
        )
    }

    private func toggleModal() {
        showModal.value.toggle()
        onTrigger?()
    }
}

它是这样使用的:

struct DetailView: View {
    @Binding var showModal: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.showModal = false
            }) {
                Text("Dismiss")
            }
        }
    }
}

struct ContentView: View {
    @State var showModal = false

    var body: some View {
        BindedPresentationButton(
            showModal: $showModal,
            label: Text("Show"),
            destination: DetailView(showModal: $showModal)
        ) {
            print("dismissed")
        }
    }
}

【讨论】:

  • 不适用于 SwiftUI 2 - 不推荐使用模式
【解决方案9】:

在 Xcode 11.0 beta 7 中,值现在被包装,以下函数对我有用:

func dismiss() {
    self.presentationMode.wrappedValue.dismiss()
}

【讨论】:

    【解决方案10】:

    Navigation 中自动弹出或在Modal 中关闭


    只需从目标视图中的环境中获取presentationMode,然后从其中获取dismiss wrappedValue

    struct DestinationView: View {
        @Environment(\.presentationMode) private var presentationMode
    
        var body: some View {
            Button("Dismiss") {
                self.presentationMode.wrappedValue.dismiss()
            }
        }
    }
    
    

    演示(弹出/关闭)

    【讨论】:

    • 感谢您发布此信息。这就是为什么PresentationMode 可能不是关闭模式的最佳解决方案,因为如果您有NavigationView,它可能会弹出到上一个视图。如果要确保关闭模式,则应传递 @State 变量。
    【解决方案11】:

    Swift 5.5 和 SwiftUI 3 中的新功能:

    @Environment(\.dismiss) var dismiss
    

    然后在函数或正文代码的某处,只需调用:

    self.dismiss()
    

    【讨论】:

      【解决方案12】:

      在您开始在 ListForm 视图中使用它们之前,SwiftUI 中的模态视图似乎很简单。我创建了一个小型库,它包含所有边缘情况并使模态视图的使用与NavigationView-NavigationLink 对相同。

      该库在此处开源:https://github.com/diniska/modal-view。您可以使用 Swift 包管理器将其包含到项目中,或者只需复制库包含的单个文件。

      您的代码的解决方案是:

      struct DetailView: View {
          var dismiss: () -> ()
          var body: some View {
              Text("Detail")
              Button(action: dismiss) {
                  Text("Click to dismiss")
              }
          }
      }
      
      struct ContentView : View {
          var body: some View {
              ModalPresenter {
                  ModalLink(destination: DetailView.init(dismiss:)) {
                      Text("Click to show")
                  }
              }
          }
      }
      

      另外,还有一篇文章有​​完整的描述和例子:How to present modal view in SwiftUI

      【讨论】:

        【解决方案13】:

        在 PresentationMode 中使用环境变量。 这个 GitHub 链接可能会帮助您解决问题https://github.com/MannaICT13/Sheet-in-SwiftUI

        这是一个简单的解决方案:

        struct ContentView2 : View {
        
            @Environment (\.presentationMode) var presentationMode
        
            var body : some View {
                VStack {
                    Text("This is ContentView2")
                    Button(action: {
                        self.presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Back")    
                    })    
                }
            }
        }
        
        
        struct ContentView: View {
        
            @State var isShowingSheet : Bool = false
        
            var body: some View {
                Button(action: {
                    self.isShowingSheet.toggle()
                }, label: {
                    Text("Click Here")
                }).sheet(isPresented: $isShowingSheet, content: {  
                    ContentView2()
                })
            }
        }
        

        【讨论】:

          【解决方案14】:

          执行此操作的一种方法可能是声明您自己的修饰符以进行模态呈现和解除。

          extension View {
          
            func showModal<T>(_ binding: Binding<Bool>, _ view: @escaping () -> T) -> some View where T: View {
          
              let windowHeightOffset = (UIApplication.shared.windows.first?.frame.height ?? 600) * -1
          
              return ZStack {
          
                self
          
                view().frame(maxWidth: .infinity, maxHeight: .infinity).edgesIgnoringSafeArea(.all).offset(x: 0, y: binding.wrappedValue ? 0 : windowHeightOffset)
          
              }
          
            }
          }
          

          然后您可以在任何您希望告诉如何显示视图和关闭该视图的视图上使用修饰符。就像弹出框或表单修饰符一样。

          struct ContentView: View {
          
            @State var showModal = false
          
            var body: some View {
          
              Text("Show").foregroundColor(.blue).onTapGesture {
                withAnimation(.easeIn(duration: 0.75)) {
                  self.showModal = true
                }
              }.showModal($showModal, {
          
                Text("Dismiss").foregroundColor(.blue).onTapGesture {
                  withAnimation(.easeIn(duration: 0.75)) {
                    self.showModal = false
                  }
                }
          
              })
          
          
            }
          }    
          

          演示文稿从顶部全屏显示,如果您希望它从侧面显示,请将修饰符内的过渡更改为前导或尾随。其他过渡也可以,例如不透明度或比例。

          【讨论】:

          • 是的,我的旧应用坏了,我会在有时间修复时更新。对不起安德鲁
          • %hugs% %hugs% %hugs%
          【解决方案15】:

          SwiftUI 2 代码示例(也适用于手机)

          (示例代码不适用于 swift 1,但您仍然可以在没有 @main 块的情况下尝试它)

          使用表格的完整应用示例:

          @main
          struct TestAppApp: App {
              var body: some Scene {
                  WindowGroup {
                      SheetLink(text: "click me!", content: ChildView() )
                          .padding(.all, 100)
                  }
              }
          }
          
          struct ChildView: View {
              var body: some View {
                  Text("this is subView!")
              }
          }
          

          当子视图大于主视图时:

          以及这背后的代码:

          struct SheetLink<Content> : View where Content: View {
              @State var text: String
              @State var displaySheet = false
              @State var content: Content
          
          
              var body: some View {
                  HStack {
                      Button(text, action: { self.displaySheet = true } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
                  }
                  .sheet(isPresented: $displaySheet) {
                      SheetTemplateView(isPresented: self.$displaySheet, content: content)
                  }
              }
          }
          
          struct SheetTemplateView<Content> : View where Content: View {
              @Binding var isPresented: Bool
              @State var content: Content
              
              var body: some View {
                  VStack{
                      HStack{
                          Button("Back!", action: { isPresented.toggle() } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
                          Spacer()
                      }
                      Spacer()
                      content
                      Spacer()
                  }
                  .padding()
              }
          }
          

          【讨论】:

            【解决方案16】:

            您可以使用演示模式关闭。 声明

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

            然后在需要时将其关闭

            self.presentationMode.wrappedValue.dismiss()
            

            【讨论】:

              【解决方案17】:

              您可以使用SheetKit 关闭所有工作表

              SheetKit().dismissAllSheets()
              

              或展示新的 UISheetPresentationController

              sheetKit.present(with: .bottomSheet){
                Text("Hello world")
              }
              

              【讨论】:

                猜你喜欢
                • 2021-06-04
                • 1970-01-01
                • 2020-02-06
                • 1970-01-01
                • 1970-01-01
                • 2020-05-06
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多