在WWDC 2019上,Apple宣布了一种新的“卡式”外观用于模态演示,并附带了内置手势,可通过向下滑动卡来消除模态视图控制器。他们还在isModalInPresentation
上引入了新的UIViewController
属性,以便您可以选择拒绝这种解雇行为。
不过,到目前为止,我还没有找到在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构造来做到这一点?
答案 0 :(得分:31)
我也想这样做,但找不到任何解决方案。劫持拖动手势的答案可能有效,但通过滚动滚动视图或滚动窗体将其消除时,此答案无效。该问题中的方法也不太灵活,因此我对其进行了进一步调查。
对于我的用例,我在工作表中有一个表单,理想情况下可以在没有内容的情况下将其关闭,但必须在有内容的情况下通过警报进行确认。
我对这个问题的解决方案:
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
@Binding var isModal: Bool
let onDismissalAttempt: (()->())?
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
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: Binding<Bool>, onDismissalAttempt: (()->())? = nil) -> some View {
ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)
}
}
这对我的用例来说是完美的,希望对您或其他人有帮助。
答案 1 :(得分:8)
注意:此代码已过编辑,以确保简洁明了。
使用一种从here获取当前窗口场景的方法,您可以通过here的扩展名@Bobj-C获取顶视图控制器
extension UIApplication {
func visibleViewController() -> UIViewController? {
guard let window = UIApplication.shared.windows.first(where: { $0.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))
}
}
}
答案 2 :(得分:2)
通过更改不想拖动的任何视图的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()
}
}
}
}
答案 3 :(得分:2)
您可以使用此方法传递模态视图的内容以供重用。
将 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.")
}
}
}
}
结果!
答案 4 :(得分:2)
从 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 上运行良好。
注意:此解决方案不允许您在满足特定条件时启用关闭手势。要使用条件启用和禁用关闭手势,请参阅我的 other answer。
答案 5 :(得分:2)
从 iOS 15 开始,我们可以使用 interactiveDismissDisabled
:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
我们只需要将它附加到工作表上:
struct ContentView: View {
@State private var showSheet = false
var body: some View {
Text("Content View")
.sheet(isPresented: $showSheet) {
Text("Sheet View")
.interactiveDismissDisabled(true)
}
}
}
如果需要,您还可以传递一个变量来控制何时可以禁用工作表:
.interactiveDismissDisabled(!userAcceptedTermsOfUse)
答案 6 :(得分:1)
这个解决方案在 iPhone 和 iPad 上对我有用。它使用 isModalInPresentation
。来自the docs:
该属性的默认值为 false。当您将其设置为 true 时,UIKit 会忽略视图控制器边界之外的事件,并防止视图控制器在屏幕上时交互式关闭。
您的尝试与对我有用的方法相近。诀窍是在 isModalInPresentation
willMove(toParent:)
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
}
}
答案 7 :(得分:1)
对于@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")
}
}
)
}
}
答案 8 :(得分:0)
我们在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)
}
}
}