Swift_借助presentingViewController
先实现三个模态跳转需求。
1. A跳转B,并从B返回A
image.png//A页面
let B = BViewController()
self.present(B, animated: true, completion: nil)
//B页面
self.dismiss(animated: true, completion: nil)
// 理论上self.dismiss这么写是错误的。正确的方法应该是
// self.presentingViewController?.dismiss(animated: true, completion: nil)
// 后面会解释 ↓↓↓↓↓↓
2. A跳转B,B跳转C,并从C返回A
image.png//A
let B = BViewController()
self.present(B, animated: true, completion: nil)
//B
let C = CViewController()
self.present(C, animated: true, completion: nil)
//C
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
3.A跳转B,B跳转C,C跳转D...,并从N返回A
image.png//N页面
let vc = self.bottomestPresentedViewController()
vc.dismiss(animated: true, completion: nil)
extension UIViewController {
/// 获取最底层的控制器
public func bottomestPresentedViewController() -> UIViewController {
var bottomestVC = self
while bottomestVC.presentingViewController != nil {
bottomestVC = bottomestVC.presentingViewController!
}
return bottomestVC
}
/// 获取最顶层的控制器
public func topestPresentedViewController() -> UIViewController {
var topestVC = self
while topestVC.presentedViewController != nil {
topestVC = topestVC.presentedViewController!
}
return topestVC
}
}
那么问题来了!
问题1:presentingViewController是什么鬼?与它对应的presentedViewController又是什么呢?
- presentingViewController: 当前控制器的上层视图。
- presentedViewController: 当前控制器的下层视图。
在案例2中(A跳转B,B返回C),不同控制器中,两个属性分别为
控制器 | presentingViewController | presentedViewController |
---|---|---|
A | nil | B |
B | A | C |
C | B | nil |
- 在A中
由于A是根视图,presentingViewController为nil。
A present 出来的B,presentedViewController值为B。 - 在B中
由于A present出来的B,presentingViewController值为A。
B present 的C,所以presentedViewController值为C。 - 在C中
由于B present出来的C,presentingViewController值为B,
C没有present控制器,所以presentedViewController值为nil。
问题2: 为什么说B中self.dismiss(animated: true, completion: nil)理论上是错误的?
谁污染,谁治理原则
A打开了B,当然是A来负责关闭!
self.presentingViewController?.dismiss(animated: true, completion: nil)
B页面调用self.dismiss(animated: true, completion: nil)
,此时的self指代的为B,由于B没有present任何页面,所以这样理论上市不对的。但是系统将dismiss
方法交给了B的presentingViewController
(也就是A)来执行。所以也能实现dismiss效果。
继续深究
问题3:是不是说一个控制器A弹出一个控制器B,A就一定是B的presentingViewController?
再来看两个例子
示例一 示例二这两个例子中的控制器B的presentingViewController
分别是谁呢?
示例1中为 导航栏控制器,示例2中为 分栏控制器。
子控制器childViewControllers
当一个控制器成为另一个控制器的子控制器时(比如:UINavigationViewController、UITabBarController、UIPageViewController),子控制器的presentedViewController和presentingViewController由父控制器的这两个属性决定。
示例一中
控制器B.presentingViewController = A.navigationController = 导航栏控制器
示例二中
控制器B.presentingViewController = A.navigationController.tabbarController = 分栏控制器
继续探究
问题4: 该报错是怎么引起的?
Warning: Attempt to present <Demo.CViewController: 0x7fc29df39bf0> on <Demo.ViewController: 0x7fc29dc0b1e0> which is already presenting <Demo.BViewController: 0x7fc29df229f0>
控制器A在已经弹出了控制器BViewController的情况下,尝试present控制器CViewController。
那么应该是谁来执行present呢?应该由B控制器(最上层的控制器)来做present。
上面代码中以给出获取最上层控制器的方法
继续探究
问题5:实现需求三的依据是什么?
官方文档的说明 附链接地址
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.
If you want to retain a reference to the view controller'��s presented view controller, get the value in the presentedViewController property before calling this method.
翻译一下
呈现视图控制器负责解散它呈现的视图控制器。如果你在被呈现的视图控制器本身上调用这个方法,UIKit会要求呈现的视图控制器处理取消。
如果你连续呈现几个视图控制器,这样就构建了一个被呈现视图控制器的堆栈,在堆栈较低的视图控制器上调用这个方法,会丢弃它的直接子视图控制器和堆栈上该子视图控制器上面的所有视图控制器。当这种情况发生时,只有最上面的视图以动画方式被取消;任何中间视图控制器都可以简单地从堆栈中删除。最上面的视图使用它的模态转换样式被取消,它可能与堆栈中其他视图控制器使用的样式不同。
如果你想保留一个引用视图控制器的��年代呈现视图控制器,得到presentedViewController属性中的值在调用这个方法之前。
大体意思是:
- 根视图负责调用dismiss来关闭它弹出来的子视图,也可以直接在子视图中调用dismiss方法,UIKit会通知根视图去处理。
- 如果连续弹出多个子视图,应当由最底层的根视图调用dismiss来一次性关闭所有子视图。
- 关闭多个子视图时,只有最顶层的子视图会有动画效果,下层的子视图会直接被移除,不会有动画效果。
- 如果想dismiss到倒数第二层,可以使用根视图的presentedViewController来调用dismiss方法。
继续探究
问题5:模态跳转和视图的生命周期
Warning: Attempt to present <UINavigationController: 0x7fb3ef821000> on <Demo.ViewController: 0x7fb3eec15fd0> whose view is not in the window hierarchy!
控制器的view还未加入到window的时候,尝试做present操作。
方法一:
通过子视图的方式加载
override func viewDidLoad() {
super.viewDidLoad()
let vc = BViewController()
vc.view.frame = view.bounds
view.addSubview(vc.view)
self.addChild(vc)
}
方法二:
当视图已经添加完成的时候
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let vc = BViewController()
self.present(vc, animated: true, completion: nil)
}
继续探究
问题5:如何自定义模态跳转的动画效果
以一个淡入淡出的动画效果为例
- 先实现动画类
class AnimatedTransition: NSObject,UIViewControllerAnimatedTransitioning {
//是否在presenting
var presenting = true
let duration = 1.0
//动画持续时间
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
//动画执行的方法
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
if presenting {
containerView.addSubview(toView!)
toView!.alpha = 0.0
UIView.animate(withDuration: duration,
animations: {
toView!.alpha = 1.0
}, completion: { _ in
transitionContext.completeTransition(true)
})
} else {
UIView.animate(withDuration: duration,
animations: {
fromView!.alpha = 0.5
}, completion: { _ in
transitionContext.completeTransition(true)
})
}
}
}
- 在执行present的控制器中
class ViewController: UIViewController {
// 初始化动画类
private let transition = AnimatedTransition()
@IBAction func presentEvent(_ sender: UIButton) {
let vc = BViewController()
vc.modalPresentationStyle = .fullScreen
// 遵守动画过渡协议
vc.transitioningDelegate = self
self.present(vc, animated: true, completion: nil)
}
}
// 实现过渡协议方法
extension ViewController: UIViewControllerTransitioningDelegate{
//dismiss的时候 使用自定义的动画
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//如果不用自定的过渡动画,就 return nil
transition.presenting = false
return transition
}
//present的时候使用自定义的动画
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//如果不用自定的过渡动画,就 return nil
transition.presenting = true
return transition
}
}