本文中介绍的方法,有可能在未来的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中不好解决的问题,都可以采用这种思想,这就是本文要教给你最重要的东西。
我们先看看最终的效果:
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个步骤:
这里就不多说了,大家看代码就行了:
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/,如有侵权,立即删除。