iOS UIPresentationController 真的完全能代替View弹窗吗?

View

先说是一个 view做一个弹窗比较容易掉的坑。
iOS 一般做一个弹窗,我们一般是创建一个view add到父view上显示出来,
代码大约是下面这样,我没有封装,不过大体都是这样,定义一个全局myView ,add到父视图,点击按钮removeFromSuperview删除。在定义一个myButton 是局部的,内部实现removeFromSuperview

  let myView = CustomView()

   override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor=UIColor.white
        // Do any additional setup after loading the view.
        myView.frame=CGRect(x: 100, y: 100, width: 200, height: 200)
        myView.backgroundColor=UIColor .red
        view.addSubview(myView)
        
        let myButton=CustomButton(frame: CGRect (x: 100, y: 350, width: 200, height:200))
        myButton.backgroundColor=UIColor .gray
        view.addSubview(myButton)

    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        myView .removeFromSuperview()
        DispatchQueue.main.asyncAfter(deadline: .now()+1, execute: {
            //removeFromSuperview之后 view 还存在内存当中没有被删除
            print(self)
        })
}
FC48613E-29A1-45D8-B835-8606B35B3D8C.png

我先点击了myButton ,在点击屏幕,myButton先执行了内部的removeFromSuperview,之后myView执行removeFromSuperview,可能很多人并没有注意,我们removeFromSuperview 后 ,其实这个myView并没有释放,我在removeFromSuperview 后 延时1秒打印当前控制器


CA3F78E1-783C-4E3D-894E-447FF95B9F2C.png
D8973BCF-7C25-4263-ADD8-9E388F5D5AAF.png

为什么执行了removeFromSuperview myView还在内存中?myButton彻底没有了
下面是苹果对于removeFromSuperview这个API的官方定义:

Unlinks the receiver from its superview and its window, and removes it from the responder chain.

译:把当前View从它的父View和窗口中移除,同时也把它从响应事件操作的响应者链中移除。

执行removeFromSuperview方法后,会从父视图中移除,并且将Superview对视图的强引用删除,此时如果没有其他地方再对视图进行强引用,则会从内存中移除。如果还存在其他强引用,视图只是不在屏幕中显示,并没有将该视图从内存中移除。所以如果需要使用该视图,不需要再次创建,而是直接addSubview就可以了。

因为我们的myView 是已经被控制引用了,所以控制器不销毁,myView也不会销毁。myButton因为没有被控制引用了,所以removeFromSuperview 内存中也移除了。
所以我们在开发中如果使用View做弹窗尽量不要有强引用。

另外我们在View做弹窗,经常把View添加到UIApplication.shared.keyWindow 上,这个也是蛮多坑的,因为我们下面主要讲UIPresentationController,这个可以参考下面两篇文章
iOS开发笔记 | 看完这篇就不会再被keyWindow坑了
iOS 面向bug开发之UIWindow出现的“穿透”问题

UIPresentationController

iOS8开始 苹果的弹窗的控件UIAlertView,UIActionSheet控件逐渐废弃, UIAlertController启用, 弹窗开始由view 像viewController类型转变 ,

UIPresentationController是 iOS8 新增的一个API,苹果的官方定义是:对象为所呈现的视图控制器提供高级视图的转换管理(从呈现视图控制器的时间直到它被消除期间)。其实说白了就是用来控制controller之间的跳转特效。比如希望实现一个特效,显示一个窗口,大小和位置都是自定义的,并且遮罩在原来的页面上

通过视图层级查看 UIAlertController 也是UIPresentationController 模态出来的
我封装的弹窗 也慢慢的 从 view转向 使用UIPresentationController模态一个viewController,真的好用,瞬间感觉弹窗优雅了起来,viewController 弹出其实最终调还是present(viewController, animated: true, completion: completion),只不过viewController.modalPresentationStyle = .custom,我们自定义了 视图弹出方式,就可以设置动画,手势,大小等等。viewController消失, 使用也是dismiss(animated: true),所有强引用的view,都会随着viewController销毁而销毁。这里推荐一个我一直常用的UIPresentationController模态封装iOS-Modal,Objective-C和Swift都有,朴实无华,没有那么多炫酷的模态动画,但也够用了。
下面是调用

  let configuration = ModalConfiguration.default
  configuration.direction = .bottom
  configuration.isEnableBackgroundAnimation=true//开启动画
  let size = CGSize(width: UIScreen.main.bounds.width, height: 300)
  let storyboard = UIStoryboard(name: "Main", bundle: nil)
  let vc = storyboard.instantiateViewController(withIdentifier: "ModalViewController")
  vc.view.backgroundColor=UIColor.red
   presentModalViewController(vc, contentSize: size, configuration: configuration,completion:nil)
41F9ADA2-75DB-4CCA-915B-5F93C5130649.png

直到有一天我发现,在一个页面有多个弹窗,并且弹窗弹出的条件都同一时间触发,只弹出一个,其他的虽然走到了 present(viewController, animated: true, completion: completion),但依然无法弹出,而且还连个报错都没有,后来我用viewController+UIAlertController 一起模态弹出 ,才看到报错如下

    override func viewDidLoad() {
        super.viewDidLoad()
        showRedVC()
    }
    func showRedVC() {
        let configuration = ModalConfiguration.default
        configuration.direction = .bottom
        configuration.isEnableBackgroundAnimation=true//开启动画
        let size = CGSize(width: UIScreen.main.bounds.width, height: 300)
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "ModalViewController")
        vc.view.backgroundColor=UIColor.red
        presentModalViewController(vc, contentSize: size, configuration: configuration,completion:{
            self.showAlertVC()
        })
    }
    func showAlertVC() {
        let alertVC = UIAlertController(title: "大家好", message: "欢迎来到德莱联盟", preferredStyle: .actionSheet)
        let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
        let okAction = UIAlertAction(title: "好的", style: .default, handler: nil)
        alertVC.addAction(cancelAction)
        alertVC.addAction(okAction)
        present(alertVC, animated: true, completion:{
            self.showGreenVC()
        })
    }
Warning: Attempt to present <UIAlertController: 0x7f8b80062000>  on <SwiftModalExample.FourthViewController: 0x7f8b7f515b60> which is already presenting (null)

最后了解到,一个视图控制器仅能使用present模态方法弹出一个控制器,这个被模态出的控制器,没有dismiss,其他的控制器无法被模态出来的的。
这就很糟糕了,我们有多个页面有好几个弹窗

  1. 比如首页,基本都有的弹窗, app升级弹窗,推送通知未打开弹窗,权限弹窗,业务弹窗,广告弹窗之类的等等,3-5弹窗都常态。
  2. 有的弹窗可能还跨页面的,比如本地推送,还有类似淘宝的淘口令弹窗,这种都是工程内的所有页面都可以显示的,
  3. 基本上每个页面都还有一些我们手动触发的弹窗,比如分享类似的业务弹窗。

如果我们都使用了UIPresentationController模态出来的viewController 作为弹窗,只要我们有一个模态弹出了,另一个就无法弹出, 而view 是加多少都没问题。
如何解决
如果一个视图控制器仅能使用present模态方法弹出一个控制器,那么我们就永远获取最上层的视图控制器,可不可以
在UIViewController的Extensions 中写个 topMostController方法 获取最上层 UIViewController

 // 获取最上层 UIViewController
    func topMostController() -> UIViewController? {
        if presentedViewController == nil {
            return self
        } else if (presentedViewController is UINavigationController) {
            let navigationController = presentedViewController as? UINavigationController
            let lastViewController = navigationController?.viewControllers.last
            return lastViewController?.topMostController()
        }

        let presentedController = presentedViewController
        return presentedController?.topMostController()
    }

使用看看,注意我们每次present 之前都会调用let topVC = topMostController() 获取最上层的UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()
        showRedVC() 
    }
    func showRedVC() {
        let configuration = ModalConfiguration.default
        configuration.direction = .top
        configuration.isEnableShadow=false
        configuration.animationDuration=0.2
        let size = CGSize(width: UIScreen.main.bounds.width, height: 300)
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "ModalViewController")
        vc.view.backgroundColor=UIColor.red
        let topVC = topMostController() // 获取最上层 UIViewController
        topVC?.presentModalViewController(vc, contentSize: size, configuration: configuration,completion:{
            self.showAlertVC()
        })
    }
    func showAlertVC() {
        let alertVC = UIAlertController(title: "大家好", message: "欢迎来到德莱联盟", preferredStyle: .actionSheet)
        let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
        let okAction = UIAlertAction(title: "好的", style: .default, handler: nil)
        alertVC.addAction(cancelAction)
        alertVC.addAction(okAction)
        let topVC = topMostController() // 获取最上层 UIViewController
        topVC?.present(alertVC, animated: true, completion:{
            self.showGreenVC()
        })
    }
    func showGreenVC() {
        let configuration = ModalConfiguration.default
        configuration.direction = .left
        configuration.isEnableShadow=false
        configuration.animationDuration=0.2
        let size = CGSize(width: 200, height: 500)
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "ModalViewController")
        vc.view.backgroundColor=UIColor.green
        let topVC = topMostController() // 获取最上层 UIViewController
        topVC?.presentModalViewController(vc, contentSize: size, configuration: configuration, completion: nil)
    }

窗口都模态弹出了


9FFD2863-1323-423A-91AB-71C4FFF8A959.png

可以看到 所有弹窗 都弹出了,可见我们的方法是有效的。

一般到这里 我们应该愉快的撒花,貌似我们的问题都解决了。其实不然,let topVC = topMostController() 获取最上层UIViewController 是解决了, present模态的窗口同一时间触发,只弹出一个的问题。但是在我们真实开发一个项目的时候,每次模态 都要写这段代码let topVC = topMostController()貌似有些麻烦,就像程序员穿格子衫,还要扎个领带,这能长久吗,特别是多人团队开发的时候,一个交接不好就可能忘了。当然我们可以继续封装,但是我感觉并不好,永远获取最上层,并不知道会不会有其他影响,比如这些弹窗都有跳转到其他页面的功能,到时候还得不断的调试。

所以我总结的最优方案

  1. 页面所有用户“主动”触发的 弹窗,比如分享,选择菜单等业务弹窗,我们都可以使用UIPresentationController模态出来的viewController 作为弹窗。
  2. 接口获取的(比如app升级提示,业务广告等),逻辑条件满足的(比如本地推送)弹窗 ,继续使用view弹窗

简单的说:主动viewController,被动view

这样用户点击的viewController弹窗和接口获取的view弹窗,在同一个页面就不在有冲突了,代码也再也没有多余调用。(我终于为我的懒惰找到了借口)

本文demo SwiftModalExample

参考
随便说说removeFromSuperview方法
iOS自定义转场动画/UIPresentationController