阅读 85

SwiftUI之如何监听Dismiss手势

本文中介绍的方法,有可能在未来的SwiftUI升级中,失去效果,但我们仍然可以使用本文中解决问题的思想,这一点很重要。

可以在这里下载完整代码gist.github.com/agelessman/…

大家先思考一个问题,假如我们想在SwiftUI中监听一个Modal试图的dismiss手势,应该怎么做?在UIKit中,很简单,但是在SwiftUI中,暂时还没有直接的方法。

UIAdaptivePresentationControllerDelegate里边有一些方法,在这种场景下很有用,比如:

    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        dismissGuardianDelegate?.attemptedUpdate(flag: true)
    }
    
    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        return !self.preventDismissal
    }
复制代码

上边是协议中的两个方法,分别可以监听dismiss和是否支持dismiss。

那么重点来了,我们现在要使用UIHostingController在SwiftUI和UIAdaptivePresentationControllerDelegate中间架起一座桥梁,换句话说,以后再遇到SwiftUI中不好解决的问题,都可以采用这种思想,这就是本文要教给你最重要的东西。

我们先看看最终的效果:

Kapture 2020-05-29 at 19.58.40.gif

protocol DismissGuardianDelegate {
    func attemptedUpdate(flag: Bool)
}

class DismissGuardianUIHostingController<Content>: UIHostingController<Content>, UIAdaptivePresentationControllerDelegate where Content: View {
    var preventDismissal: Bool
    var dismissGuardianDelegate: DismissGuardianDelegate?
    
    init(rootView: Content, preventDismissal: Bool) {
        self.preventDismissal = preventDismissal
        super.init(rootView: rootView)
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        viewControllerToPresent.presentationController?.delegate = self
        dismissGuardianDelegate?.attemptedUpdate(flag: false)
        super.present(viewControllerToPresent, animated: flag, completion: completion)
    }
    
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        dismissGuardianDelegate?.attemptedUpdate(flag: true)
    }
    
    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        return !self.preventDismissal
    }
}
复制代码

这里有一个比较重要的内容,当我们在SwiftUI中通过sheet,present出一个新的界面的时候,SwiftUI会使用距离该sheet最近的一个controller做presentationController,这里有什么区别呢?举两个例子:

NavigationView {
    DismissGuardian(preventDismissal: $preventDismissal, attempted: $attempted) {
        Text("hi").sheet(isPresented: self.$modal1, content: { MyModal().environmentObject(self.model) })
    }
}
复制代码

这种情况下,由于sheet写在了Text中,所以最近的presentationController是DismissGuardian。

DismissGuardian(preventDismissal: $preventDismissal, attempted: $attempted) {
    NavigationView {
        Text("hi").sheet(isPresented: self.$modal1, content: { MyModal().environmentObject(self.model) })
    }
}
复制代码

如果代码是这样,sheet最近的presentationController就是NavigationView了,也就是导航控制器。

这里边的区别就是下边的这种情况我们无法监听到Dismiss手势,原因是:

   override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        viewControllerToPresent.presentationController?.delegate = self
        dismissGuardianDelegate?.attemptedUpdate(flag: false)
        super.present(viewControllerToPresent, animated: flag, completion: completion)
    }
复制代码

我们把viewControllerToPresent的presentationController赋值给了DismissGuardianUIHostingController。

大家可以这样理解这一块的内容,DismissGuardianUIHostingController做为一个容器,它里边放着SwiftUI中的View。

struct ContentView: View {
    @State private var show = false
    @ObservedObject var dataModel = MyDataModel()
    
    var body: some View {
        DissmissGuardian(preventDismiss: $dataModel.preventDissmissal, attempted: $dataModel.attempted) {
            VStack {
                Spacer()
                
                Text("演示如何监听Dissmiss手势").font(.title)
                
                Spacer()
                
                Button("跳转到新的View") {
                    self.show = true
                }
                .sheet(isPresented: self.$show) {
                    MyCustomerView().environmentObject(self.dataModel)
                }
                
                Spacer()
            }
        }
    }
}
复制代码

如果大家理解起来有困难,可以留言。DismissGuardianUIHostingController是不能直接显示在SwiftUI中的body中的,需要通过UIViewControllerRepresentable转换一层。

如何转换,基本上也是固定的写法,包含3个步骤:

  • 初始化
  • makeUIViewController,updateUIViewController
  • makeCoordinator,Coordinator

这里就不多说了,大家看代码就行了:

struct DissmissGuardian<Content: View>: UIViewControllerRepresentable {
    @Binding var preventDismiss: Bool
    @Binding var attempted: Bool
    var content: Content
    
    init(preventDismiss: Binding<Bool>, attempted: Binding<Bool>, @ViewBuilder content: @escaping () -> Content) {
        self._preventDismiss = preventDismiss
        self._attempted = attempted
        self.content = content()
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        return DismissGuardianUIHostingController(rootView: self.content, preventDismissal: self.preventDismiss)
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let dismissHosting = uiViewController as! DismissGuardianUIHostingController<Content>
        dismissHosting.preventDismissal = self.preventDismiss
        dismissHosting.rootView = self.content
        dismissHosting.dismissGuardianDelegate = context.coordinator
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(attempted: $attempted)
    }
    
    class Coordinator: NSObject, DismissGuardianDelegate {
        @Binding var attempted: Bool
        
        init(attempted: Binding<Bool>) {
            self._attempted = attempted
        }
        
        func attemptedUpdate(flag: Bool) {
            self.attempted = flag
        }
    }
}
复制代码

最后总结一下,凡是遇到在SwiftUI中很难实现的功能,在UIKit中很容易实现,就考虑这种方法。

注:上边的内容参考了网站https://swiftui-lab.com/modal-dismiss-gesture/,如有侵权,立即删除。