SwiftUI之如何监听Dismiss手势
本文中介绍的方法,有可能在未来的SwiftUI升级中,失去效果,但我们仍然可以使用本文中解决问题的思想,这一点很重要。
可以在这里下载完整代码https://gist.github.com/agelessman/335914b2db480c5a343111a4a5bd4e36
大家先思考一个问题,假如我们想在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中不好解决的问题,都可以采用这种思想,这就是本文要教给你最重要的东西。
我们先看看最终的效果:
![](https://img.haomeiwen.com/i1914952/79079126420a54fc.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/,如有侵权,立即删除。