防止在SwiftUI中解雇模式视图控制器

时间:2019-06-16 01:27:09

标签: ios swift swiftui

在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构造来做到这一点?

9 个答案:

答案 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.")
       }
    }
  }
}

结果!

enter image description here

答案 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

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