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)
})
}
我先点击了myButton ,在点击屏幕,myButton先执行了内部的removeFromSuperview,之后myView执行removeFromSuperview,可能很多人并没有注意,我们removeFromSuperview 后 ,其实这个myView并没有释放,我在removeFromSuperview 后 延时1秒打印当前控制器
为什么执行了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)
直到有一天我发现,在一个页面有多个弹窗,并且弹窗弹出的条件都同一时间触发,只弹出一个,其他的虽然走到了 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,其他的控制器无法被模态出来的的。
这就很糟糕了,我们有多个页面有好几个弹窗
- 比如首页,基本都有的弹窗, app升级弹窗,推送通知未打开弹窗,权限弹窗,业务弹窗,广告弹窗之类的等等,3-5弹窗都常态。
- 有的弹窗可能还跨页面的,比如本地推送,还有类似淘宝的淘口令弹窗,这种都是工程内的所有页面都可以显示的,
- 基本上每个页面都还有一些我们手动触发的弹窗,比如分享类似的业务弹窗。
如果我们都使用了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)
}
窗口都模态弹出了
可以看到 所有弹窗 都弹出了,可见我们的方法是有效的。
一般到这里 我们应该愉快的撒花,貌似我们的问题都解决了。其实不然,let topVC = topMostController() 获取最上层UIViewController 是解决了, present模态的窗口同一时间触发,只弹出一个的问题。但是在我们真实开发一个项目的时候,每次模态 都要写这段代码let topVC = topMostController()貌似有些麻烦,就像程序员穿格子衫,还要扎个领带,这能长久吗,特别是多人团队开发的时候,一个交接不好就可能忘了。当然我们可以继续封装,但是我感觉并不好,永远获取最上层,并不知道会不会有其他影响,比如这些弹窗都有跳转到其他页面的功能,到时候还得不断的调试。
所以我总结的最优方案
- 页面所有用户“主动”触发的 弹窗,比如分享,选择菜单等业务弹窗,我们都可以使用UIPresentationController模态出来的viewController 作为弹窗。
- 接口获取的(比如app升级提示,业务广告等),逻辑条件满足的(比如本地推送)弹窗 ,继续使用view弹窗
简单的说:主动viewController,被动view
这样用户点击的viewController弹窗和接口获取的view弹窗,在同一个页面就不在有冲突了,代码也再也没有多余调用。(我终于为我的懒惰找到了借口)
本文demo SwiftModalExample
参考
随便说说removeFromSuperview方法
iOS自定义转场动画/UIPresentationController