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

Jum*_*hyn 8 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
    }
}
Run Code Online (Sandbox Code Playgroud)

即使在出席会议之后,.presentation(Modal(MyViewControllerView()))我仍然可以向下滑动以消除视图。当前是否可以使用现有的SwiftUI构造来做到这一点?

Gui*_*iks 56

iOS 15 更新

根据下面pawello2222回答,新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)
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

iOS-15 之前的答案

我也想这样做,但在任何地方都找不到解决方案。劫持拖动手势的答案有点有效,但在通过滚动滚动视图或表单将其解除时则无效。问题中的方法也不那么笨拙,所以我进一步调查了它。

对于我的用例,我在工作表中有一个表单,理想情况下可以在没有内容时关闭该表单,但在有内容时必须通过警报进行确认。

我对这个问题的解决方案:

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")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

状态值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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这非常适合我的用例,希望它也能帮助你或其他人。

  • 来自一位感激的读者的建议:`isModal` 不应该是一个 Binding,因为它是只读的。删除 @Binding 会破坏这一点,因为协调器只会存储“isModal”的初始值。要解决此问题,您可以将协调器中的“modalView”设置为“var”,然后在“updateUIViewController”中更新它,例如“context.coordinator.modalView = self”,这样如果“isModal”更改,它将正确更新。关于状态变量未更新的注释可以通过在`updateUIViewController`中执行`uiViewController.rootView = view`来修复,否则视图将无法正常更新。 (8认同)
  • 这就是做到这一点的方法。没有什么老套而且非常优雅。谢谢吉多。 (4认同)
  • @jjatie我没有使用 \.presentationMode ,而是使用对使工作表可见的变量的绑定,在这种情况下 `.sheet(isPresented: $showSheet)` 所以放置 `@Binding var showSheet: Bool` 并使 `self .showSheet = false` 来关闭而不是 `self.presentationMode.wrappedValue.dismiss()` (3认同)
  • 由于某些奇怪的原因,这不适用于 NavigationView 甚至简单的 Text。不过,它可以与 Form 一起使用。 (2认同)
  • 除了@Helam评论中的更改之外,我还必须使用`UIHostingController`的子类来覆盖`willMove(to:parent)`来设置父级的`presentationController`,因为`updateUIViewController`在第一次之前没有发生我试图关闭视图控制器。 (2认同)

FRI*_*DAY 13

通过更改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()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)
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()
        }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 目前,这是我见过的最好的解决方案。 (2认同)
  • 是的,我明白你的观点(这个答案背后的想法),问题是“防止在 SwiftUI 中取消模态视图控制器”,所以如果用两根手指拖动会取消工作表,那么答案似乎不完整,而且,实现这种防止被解雇的逻辑成为复杂视图的噩梦。 (2认同)

R. *_* J. 11

注意:为了清晰和简洁,此代码已被编辑。

用一种方式来获得当前窗口的场景,从这里,你可以通过这个扩展获得顶视图控制器这里@ 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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并将其变成这样的视图修饰符:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

并使用类似:

struct ShowSheetView: View {
    @State private var showSheet = true
    var body: some View {
        Text("Hello, World!")
        .sheet(isPresented: $showSheet) {
            TestView()
                .modifier(DisableModalDismiss(disabled: true))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 9

对于每个对 @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)
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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")
                }
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)


paw*_*222 6

iOS 15+

从 iOS 15 开始我们可以使用interactiveDismissDisabled

func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
Run Code Online (Sandbox Code Playgroud)

我们只需要把它附加到工作表上:

struct ContentView: View {
    @State private var showSheet = false

    var body: some View {
        Text("Content View")
            .sheet(isPresented: $showSheet) {
                Text("Sheet View")
                    .interactiveDismissDisabled(true)
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果需要,您还可以传递一个变量来控制何时可以禁用工作表:

.interactiveDismissDisabled(!userAcceptedTermsOfUse)
Run Code Online (Sandbox Code Playgroud)


Sli*_*ron 5

从 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()
                }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意fullScreenCover在 macOS 上不可用,但在 iPhone 和 iPad 上运行良好。

注意:此解决方案不允许您在满足特定条件时启用关闭手势。要根据条件启用和禁用关闭手势,请参阅我的另一个答案