防止在 SwiftUI 中关闭模态视图控制器
Prevent dismissal of modal view controller in SwiftUI
在 WWDC 2019 上,Apple 宣布了一种新的 "card-style" 模态演示外观,它带来了内置手势,可以通过在卡片上向下滑动来关闭模态视图控制器。他们还在 UIViewController
上引入了新的 isModalInPresentation
属性,这样您就可以选择禁止这种解雇行为。
不过,到目前为止,我还没有找到在 SwiftUI 中模拟这种行为的方法。据我所知,使用 .presentation(_ modal: Modal?)
不允许您以相同的方式禁用关闭手势。我还尝试将模态视图控制器放在 UIViewControllerRepresentable
View
中,但这似乎也无济于事:
struct MyViewControllerView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyViewControllerView>) -> UIHostingController<MyView> {
return UIHostingController(rootView: MyView())
}
func updateUIViewController(_ uiViewController: UIHostingController<MyView>, context: UIViewControllerRepresentableContext<MyViewControllerView>) {
uiViewController.isModalInPresentation = true
}
}
即使在使用 .presentation(Modal(MyViewControllerView()))
进行演示后,我也能够向下滑动以关闭视图。目前有什么方法可以使用现有的 SwiftUI 结构来做到这一点吗?
通过更改您不想被拖动的任何视图的 gesture priority
,您可以阻止任何视图上的 DragGesture
。例如,对于 Modal,可以按以下方式完成:
也许这不是最佳做法,但效果很好
struct ContentView: View {
@State var showModal = true
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}.sheet(isPresented: self.$showModal) {
ModalView()
}
}
}
struct ModalView : View {
@Environment(\.presentationMode) var presentationMode
let dg = DragGesture()
var body: some View {
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.highPriorityGesture(dg)
Button("Dismiss Modal") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
注意:为清楚和简洁起见,已编辑此代码。
使用一种方法从 you can get the top view controller by this extension here from @Bobj-C
获取当前window场景
extension UIApplication {
func visibleViewController() -> UIViewController? {
guard let window = UIApplication.shared.windows.first(where: { [=10=].isKeyWindow }) else { return nil }
guard let rootViewController = window.rootViewController else { return nil }
return UIApplication.getVisibleViewControllerFrom(vc: rootViewController)
}
private static func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
if let navigationController = vc as? UINavigationController,
let visibleController = navigationController.visibleViewController {
return UIApplication.getVisibleViewControllerFrom( vc: visibleController )
} else if let tabBarController = vc as? UITabBarController,
let selectedTabController = tabBarController.selectedViewController {
return UIApplication.getVisibleViewControllerFrom(vc: selectedTabController )
} else {
if let presentedViewController = vc.presentedViewController {
return UIApplication.getVisibleViewControllerFrom(vc: presentedViewController)
} else {
return vc
}
}
}
}
并将其变成这样的视图修饰符:
struct DisableModalDismiss: ViewModifier {
let disabled: Bool
func body(content: Content) -> some View {
disableModalDismiss()
return AnyView(content)
}
func disableModalDismiss() {
guard let visibleController = UIApplication.shared.visibleViewController() else { return }
visibleController.isModalInPresentation = disabled
}
}
并像这样使用:
struct ShowSheetView: View {
@State private var showSheet = true
var body: some View {
Text("Hello, World!")
.sheet(isPresented: $showSheet) {
TestView()
.modifier(DisableModalDismiss(disabled: true))
}
}
}
更新 iOS 15
根据下面的 pawello2222's ,新 interactiveDismissDisabled(_:)
API.
现在支持此功能
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}
前iOS-15个答案
我也想这样做,但找不到任何解决方案。劫持拖动手势的答案有点管用,但当它通过滚动滚动视图或表单被取消时就不行了。问题中的方法也不那么hacky,所以我进一步调查了它。
对于我的用例,我有一个 sheet 形式的表单,理想情况下可以在没有内容时将其关闭,但在有内容时必须通过警报进行确认。
我对这个问题的解决方案:
struct ModalSheetTest: View {
@State private var showModally = false
@State private var showSheet = false
var body: some View {
Form {
Toggle(isOn: self.$showModally) {
Text("Modal")
}
Button(action: { self.showSheet = true}) {
Text("Show sheet")
}
}
.sheet(isPresented: $showSheet) {
Form {
Button(action: { self.showSheet = false }) {
Text("Hide me")
}
}
.presentation(isModal: self.showModally) {
print("Attempted to dismiss")
}
}
}
}
状态值showModally
决定是否必须模态显示。如果是这样,将其向下拖动以关闭只会触发示例中仅打印“尝试关闭”的关闭,但可用于显示确认关闭的警报。
struct ModalView<T: View>: UIViewControllerRepresentable {
let view: T
let isModal: Bool
let onDismissalAttempt: (()->())?
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
context.coordinator.modalView = self
uiViewController.rootView = view
uiViewController.parent?.presentationController?.delegate = context.coordinator
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
let modalView: ModalView
init(_ modalView: ModalView) {
self.modalView = modalView
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
!modalView.isModal
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
modalView.onDismissalAttempt?()
}
}
}
extension View {
func presentation(isModal: Bool, onDismissalAttempt: (()->())? = nil) -> some View {
ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)
}
}
这非常适合我的用例,希望它也能帮助您或其他人。
我们在 https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
创建了一个扩展,可以轻松控制模态解除
/// Example:
struct ContentView: View {
@State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}
您可以使用此方法传递模态视图的内容以供重复使用。
将 NavigationView 与 gesture priority
结合使用以禁用 dragging
。
import SwiftUI
struct ModalView<Content: View>: View
{
@Environment(\.presentationMode) var presentationMode
let content: Content
let title: String
let dg = DragGesture()
init(title: String, @ViewBuilder content: @escaping () -> Content) {
self.content = content()
self.title = title
}
var body: some View
{
NavigationView
{
ZStack (alignment: .top)
{
self.content
}
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .principal, content: {
Text(title)
})
ToolbarItem(placement: .navigationBarTrailing, content: {
Button("Done") {
self.presentationMode.wrappedValue.dismiss()
}
})
})
}
.highPriorityGesture(dg)
}
}
在内容视图中:
struct ContentView: View {
@State var showModal = true
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}.sheet(isPresented: self.$showModal) {
ModalView (title: "Title") {
Text("Prevent dismissal of modal view.")
}
}
}
}
结果!
此解决方案在 iPhone 和 iPad 上对我有效。它使用 isModalInPresentation
。来自 the docs:
The default value of this property is false. When you set it to true, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.
您的尝试与对我有用的方法很接近。诀窍是在 willMove(toParent:)
中的托管控制器的 parent 上设置 isModalInPresentation
class MyHostingController<Content: View>: UIHostingController<Content> {
var canDismissSheet = true
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
parent?.isModalInPresentation = !canDismissSheet
}
}
struct MyViewControllerView<Content: View>: UIViewControllerRepresentable {
let content: Content
let canDismissSheet: Bool
func makeUIViewController(context: Context) -> UIHostingController<Content> {
let viewController = MyHostingController(rootView: content)
viewController.canDismissSheet = canDismissSheet
return viewController
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
uiViewController.parent?.isModalInPresentation = !canDismissSheet
}
}
从 iOS 14 开始,如果您不想要关闭手势,可以使用 .fullScreenCover(isPresented:, content:)
(Docs) 而不是 .sheet(isPresented:, content:)
。
struct FullScreenCoverPresenterView: View {
@State private var isPresenting = false
var body: some View {
Button("Present Full-Screen Cover") {
isPresenting.toggle()
}
.fullScreenCover(isPresented: $isPresenting) {
Text("Tap to Dismiss")
.onTapGesture {
isPresenting.toggle()
}
}
}
}
注意:fullScreenCover
在 macOS 上不可用,但在 iPhone 和 iPad 上运行良好。
注意:此解决方案不允许您在满足特定条件时启用解雇手势。要启用和禁用带条件的解雇手势,请参阅我的 。
iOS 15
从iOS15开始我们可以使用interactiveDismissDisabled
:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
我们只需要将它附加到 sheet:
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}
如果需要,您还可以传递一个变量来控制何时可以禁用 sheet:
.interactiveDismissDisabled(!userAcceptedTermsOfUse)
致所有对@Guido 的解决方案和 NavigationView 有疑问的人。只需结合@Guido 和@SlimeBaron
的解决方案
class ModalHostingController<Content: View>: UIHostingController<Content>, UIAdaptivePresentationControllerDelegate {
var canDismissSheet = true
var onDismissalAttempt: (() -> ())?
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
parent?.presentationController?.delegate = self
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
canDismissSheet
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
onDismissalAttempt?()
}
}
struct ModalView<T: View>: UIViewControllerRepresentable {
let view: T
let canDismissSheet: Bool
let onDismissalAttempt: (() -> ())?
func makeUIViewController(context: Context) -> ModalHostingController<T> {
let controller = ModalHostingController(rootView: view)
controller.canDismissSheet = canDismissSheet
controller.onDismissalAttempt = onDismissalAttempt
return controller
}
func updateUIViewController(_ uiViewController: ModalHostingController<T>, context: Context) {
uiViewController.rootView = view
uiViewController.canDismissSheet = canDismissSheet
uiViewController.onDismissalAttempt = onDismissalAttempt
}
}
extension View {
func interactiveDismiss(canDismissSheet: Bool, onDismissalAttempt: (() -> ())? = nil) -> some View {
ModalView(
view: self,
canDismissSheet: canDismissSheet,
onDismissalAttempt: onDismissalAttempt
).edgesIgnoringSafeArea(.all)
}
}
用法:
struct ContentView: View {
@State var isPresented = false
@State var canDismissSheet = false
var body: some View {
Button("Tap me") {
isPresented = true
}
.sheet(
isPresented: $isPresented,
content: {
NavigationView {
Text("Hello World")
}
.interactiveDismiss(canDismissSheet: canDismissSheet) {
print("attemptToDismissHandler")
}
}
)
}
}
在 WWDC 2019 上,Apple 宣布了一种新的 "card-style" 模态演示外观,它带来了内置手势,可以通过在卡片上向下滑动来关闭模态视图控制器。他们还在 UIViewController
上引入了新的 isModalInPresentation
属性,这样您就可以选择禁止这种解雇行为。
不过,到目前为止,我还没有找到在 SwiftUI 中模拟这种行为的方法。据我所知,使用 .presentation(_ modal: Modal?)
不允许您以相同的方式禁用关闭手势。我还尝试将模态视图控制器放在 UIViewControllerRepresentable
View
中,但这似乎也无济于事:
struct MyViewControllerView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyViewControllerView>) -> UIHostingController<MyView> {
return UIHostingController(rootView: MyView())
}
func updateUIViewController(_ uiViewController: UIHostingController<MyView>, context: UIViewControllerRepresentableContext<MyViewControllerView>) {
uiViewController.isModalInPresentation = true
}
}
即使在使用 .presentation(Modal(MyViewControllerView()))
进行演示后,我也能够向下滑动以关闭视图。目前有什么方法可以使用现有的 SwiftUI 结构来做到这一点吗?
通过更改您不想被拖动的任何视图的 gesture priority
,您可以阻止任何视图上的 DragGesture
。例如,对于 Modal,可以按以下方式完成:
也许这不是最佳做法,但效果很好
struct ContentView: View {
@State var showModal = true
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}.sheet(isPresented: self.$showModal) {
ModalView()
}
}
}
struct ModalView : View {
@Environment(\.presentationMode) var presentationMode
let dg = DragGesture()
var body: some View {
ZStack {
Rectangle()
.fill(Color.white)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.highPriorityGesture(dg)
Button("Dismiss Modal") {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
注意:为清楚和简洁起见,已编辑此代码。
使用一种方法从
extension UIApplication {
func visibleViewController() -> UIViewController? {
guard let window = UIApplication.shared.windows.first(where: { [=10=].isKeyWindow }) else { return nil }
guard let rootViewController = window.rootViewController else { return nil }
return UIApplication.getVisibleViewControllerFrom(vc: rootViewController)
}
private static func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
if let navigationController = vc as? UINavigationController,
let visibleController = navigationController.visibleViewController {
return UIApplication.getVisibleViewControllerFrom( vc: visibleController )
} else if let tabBarController = vc as? UITabBarController,
let selectedTabController = tabBarController.selectedViewController {
return UIApplication.getVisibleViewControllerFrom(vc: selectedTabController )
} else {
if let presentedViewController = vc.presentedViewController {
return UIApplication.getVisibleViewControllerFrom(vc: presentedViewController)
} else {
return vc
}
}
}
}
并将其变成这样的视图修饰符:
struct DisableModalDismiss: ViewModifier {
let disabled: Bool
func body(content: Content) -> some View {
disableModalDismiss()
return AnyView(content)
}
func disableModalDismiss() {
guard let visibleController = UIApplication.shared.visibleViewController() else { return }
visibleController.isModalInPresentation = disabled
}
}
并像这样使用:
struct ShowSheetView: View {
@State private var showSheet = true
var body: some View {
Text("Hello, World!")
.sheet(isPresented: $showSheet) {
TestView()
.modifier(DisableModalDismiss(disabled: true))
}
}
}
更新 iOS 15
根据下面的 pawello2222's interactiveDismissDisabled(_:)
API.
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}
前iOS-15个答案
我也想这样做,但找不到任何解决方案。劫持拖动手势的答案有点管用,但当它通过滚动滚动视图或表单被取消时就不行了。问题中的方法也不那么hacky,所以我进一步调查了它。
对于我的用例,我有一个 sheet 形式的表单,理想情况下可以在没有内容时将其关闭,但在有内容时必须通过警报进行确认。
我对这个问题的解决方案:
struct ModalSheetTest: View {
@State private var showModally = false
@State private var showSheet = false
var body: some View {
Form {
Toggle(isOn: self.$showModally) {
Text("Modal")
}
Button(action: { self.showSheet = true}) {
Text("Show sheet")
}
}
.sheet(isPresented: $showSheet) {
Form {
Button(action: { self.showSheet = false }) {
Text("Hide me")
}
}
.presentation(isModal: self.showModally) {
print("Attempted to dismiss")
}
}
}
}
状态值showModally
决定是否必须模态显示。如果是这样,将其向下拖动以关闭只会触发示例中仅打印“尝试关闭”的关闭,但可用于显示确认关闭的警报。
struct ModalView<T: View>: UIViewControllerRepresentable {
let view: T
let isModal: Bool
let onDismissalAttempt: (()->())?
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
context.coordinator.modalView = self
uiViewController.rootView = view
uiViewController.parent?.presentationController?.delegate = context.coordinator
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
let modalView: ModalView
init(_ modalView: ModalView) {
self.modalView = modalView
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
!modalView.isModal
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
modalView.onDismissalAttempt?()
}
}
}
extension View {
func presentation(isModal: Bool, onDismissalAttempt: (()->())? = nil) -> some View {
ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)
}
}
这非常适合我的用例,希望它也能帮助您或其他人。
我们在 https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
创建了一个扩展,可以轻松控制模态解除/// Example:
struct ContentView: View {
@State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}
您可以使用此方法传递模态视图的内容以供重复使用。
将 NavigationView 与 gesture priority
结合使用以禁用 dragging
。
import SwiftUI
struct ModalView<Content: View>: View
{
@Environment(\.presentationMode) var presentationMode
let content: Content
let title: String
let dg = DragGesture()
init(title: String, @ViewBuilder content: @escaping () -> Content) {
self.content = content()
self.title = title
}
var body: some View
{
NavigationView
{
ZStack (alignment: .top)
{
self.content
}
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .principal, content: {
Text(title)
})
ToolbarItem(placement: .navigationBarTrailing, content: {
Button("Done") {
self.presentationMode.wrappedValue.dismiss()
}
})
})
}
.highPriorityGesture(dg)
}
}
在内容视图中:
struct ContentView: View {
@State var showModal = true
var body: some View {
Button(action: {
self.showModal.toggle()
}) {
Text("Show Modal")
}.sheet(isPresented: self.$showModal) {
ModalView (title: "Title") {
Text("Prevent dismissal of modal view.")
}
}
}
}
结果!
此解决方案在 iPhone 和 iPad 上对我有效。它使用 isModalInPresentation
。来自 the docs:
The default value of this property is false. When you set it to true, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.
您的尝试与对我有用的方法很接近。诀窍是在 willMove(toParent:)
isModalInPresentation
class MyHostingController<Content: View>: UIHostingController<Content> {
var canDismissSheet = true
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
parent?.isModalInPresentation = !canDismissSheet
}
}
struct MyViewControllerView<Content: View>: UIViewControllerRepresentable {
let content: Content
let canDismissSheet: Bool
func makeUIViewController(context: Context) -> UIHostingController<Content> {
let viewController = MyHostingController(rootView: content)
viewController.canDismissSheet = canDismissSheet
return viewController
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
uiViewController.parent?.isModalInPresentation = !canDismissSheet
}
}
从 iOS 14 开始,如果您不想要关闭手势,可以使用 .fullScreenCover(isPresented:, content:)
(Docs) 而不是 .sheet(isPresented:, content:)
。
struct FullScreenCoverPresenterView: View {
@State private var isPresenting = false
var body: some View {
Button("Present Full-Screen Cover") {
isPresenting.toggle()
}
.fullScreenCover(isPresented: $isPresenting) {
Text("Tap to Dismiss")
.onTapGesture {
isPresenting.toggle()
}
}
}
}
注意:fullScreenCover
在 macOS 上不可用,但在 iPhone 和 iPad 上运行良好。
注意:此解决方案不允许您在满足特定条件时启用解雇手势。要启用和禁用带条件的解雇手势,请参阅我的
iOS 15
从iOS15开始我们可以使用interactiveDismissDisabled
:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
我们只需要将它附加到 sheet:
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}
如果需要,您还可以传递一个变量来控制何时可以禁用 sheet:
.interactiveDismissDisabled(!userAcceptedTermsOfUse)
致所有对@Guido 的解决方案和 NavigationView 有疑问的人。只需结合@Guido 和@SlimeBaron
的解决方案class ModalHostingController<Content: View>: UIHostingController<Content>, UIAdaptivePresentationControllerDelegate {
var canDismissSheet = true
var onDismissalAttempt: (() -> ())?
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
parent?.presentationController?.delegate = self
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
canDismissSheet
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
onDismissalAttempt?()
}
}
struct ModalView<T: View>: UIViewControllerRepresentable {
let view: T
let canDismissSheet: Bool
let onDismissalAttempt: (() -> ())?
func makeUIViewController(context: Context) -> ModalHostingController<T> {
let controller = ModalHostingController(rootView: view)
controller.canDismissSheet = canDismissSheet
controller.onDismissalAttempt = onDismissalAttempt
return controller
}
func updateUIViewController(_ uiViewController: ModalHostingController<T>, context: Context) {
uiViewController.rootView = view
uiViewController.canDismissSheet = canDismissSheet
uiViewController.onDismissalAttempt = onDismissalAttempt
}
}
extension View {
func interactiveDismiss(canDismissSheet: Bool, onDismissalAttempt: (() -> ())? = nil) -> some View {
ModalView(
view: self,
canDismissSheet: canDismissSheet,
onDismissalAttempt: onDismissalAttempt
).edgesIgnoringSafeArea(.all)
}
}
用法:
struct ContentView: View {
@State var isPresented = false
@State var canDismissSheet = false
var body: some View {
Button("Tap me") {
isPresented = true
}
.sheet(
isPresented: $isPresented,
content: {
NavigationView {
Text("Hello World")
}
.interactiveDismiss(canDismissSheet: canDismissSheet) {
print("attemptToDismissHandler")
}
}
)
}
}