自定义展示型控制器
文章目录
【注意】最后更新于 February 17, 2017,文中内容可能已过时,请谨慎使用。
{% github it-boyer PresentationsDemo 2cce4c908 width = 30% %}
触发转场的方式
官方支持的自定义转场
- 在
UINavigationController
中push
和pop
; - 在
UITabBarController
中切换Tab
; - Modal 转场:
presentation
和dismissal
,俗称视图控制器的模态显示和消失,仅限于modalPresentationStyle
属性为UIModalPresentationFullScreen
或UIModalPresentationCustom
这两种模式;UICollectionViewController
的布局转场:仅限于UICollectionViewController
与UINavigationController
结合的转场方式,与上面三种都有点不同,不过实现很简单,可跳转至该链接查看。 官方的支持包含了 iOS 中的大部分转场方式,还有一种自定义容器中的转场并没有得到系统的直接支持,不过借助协议这种灵活的方式,我们依然能够实现对自定义容器控制器转场的定制,在压轴环节我们将实现这一点。
相关触发转场的动作
UINavigationController
UINavigationController
中所有修改其viewControllers
栈中 VC
的方法都可以自定义转场动画:
{% codeblock swift lang:swift %}
//我们使用的最广泛的 push 和 pop 方法
func pushViewController(_ viewController: UIViewController, animated animated: Bool)
func popViewControllerAnimated(_ animated: Bool) -> UIViewController?
//不怎么常用的 pop 方法
func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]?
func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]?
//这个方法有有点特别,是对 VC 栈的整体更新,开启动画后的执行比较复杂,具体参考文档说明。不建议在这种情况下开启转场动画。
func setViewControllers(_ viewControllers: [UIViewController], animated animated: Bool)
{% endcodeblock %}
UITabBarController
{% codeblock swift lang:swift %} //注意传递的参数必须是其下的子 VC unowned(unsafe) var selectedViewController: UIViewController? var selectedIndex: Int //和上面类似的整体更新 func setViewControllers(_ viewControllers: [UIViewController]?, animated animated: Bool) {% endcodeblock %}
Modal 转场:
{% codeblock swift lang:swift %} // Presentation 转场 func presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?) // Dismissal 转场 func dismissViewControllerAnimated(_ flag: Bool, completion completion: (() -> Void)?) {% endcodeblock %}
Segue
在 storyboard
里设置 segue
有两种方式:Button to VC
,这种在点击 Button
的时候触发转场;VC to VC
,这种需要在代码中调用performSegueWithIdentifier:sender:
。prepareForSegue:sender:
方法是在转场发生前修改转场参数的最后机会。这点对于 Modal
转场比较重要,因为在 storyboard
里 Modal
转场的 Segue
类型不支持选择 Custom
模式,使用 segue
方式触发时必须在prepareForSegue:sender:
里修改模式。
iOS 8 的变化
iOS 8 引入了适应性布局,由此添加了两种新的方式来显示一个视图控制器:
{% codeblock swift lang:swift %}
func showViewController(_ vc: UIViewController, sender sender: AnyObject?)
func showDetailViewController(_ vc: UIViewController, sender sender: AnyObject?)
{% endcodeblock %}
这两个方法咋看上去是给 UISplitViewController
用的,在 storyboard
里 segue
的候选模式里,直接给出了Show(e.g. Push)
和Show Detail(e.g. Replace)
这样的提示,以至于我之前一直对这两个 segue 有误解。实际上这两个方法智能判断当前的显示环境来决定如何显示,iOS 8 想统一显示视图控制器的方式,不过引入这两个方法增加了使用的复杂性,来看看这两个方法的使用规则。
这两个方法在 UISplitViewController
上的确是按名字显示的那样去工作的,而在本文关注的控制器上是这样工作的:
ViewController | NavigationController | TabBarController | |
---|---|---|---|
showViewController:sender: | Presentation | Push | Presentation(by self) |
showDetailViewController:sender: | Presentation | Presentation(by self) | Presentation(by self) |
UINavigationController 重写了showViewController:sender: 而执行 push 操作,上面的by self 意思是用容器 VC 本身而非其下子 VC 去执行 presentation 。这两个方法的行为可以通过重写来改变。 | |||
当非容器类 VC 内嵌在这两种容器 VC 里时,会通过最近的容器 VC 来执行: | |||
VC in NavigationController | VC in TabBarController | ||
:——- | :——- | :——– | |
showViewController:sender: | Push(by NavigationController) | Presentation(by TabBarController) | |
showDetailViewController:sender: | Presentation(by NavigationController) | Presentation(by TabBarController) |
转场五大工具
iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。转场协议由5种协议组成,在实际中只需要我们提供其中的两个或三个便能实现绝大部分的转场动画:
转场代理(Transition Delegate):
有如下三种容器转场代理,对应上面三种类型的转场:
{% codeblock lang:swift %}
动画控制器(Animation Controller):
最重要的部分,负责添加视图以及执行动画;遵守
交互控制器(Interaction Controller):
通过交互手段,通常是手势来驱动动画控制器实现的动画,使得用户能够控制整个过程;遵守
转场环境(Transition Context):
提供转场中需要的数据;遵守
转场协调器(Transition Coordinator):
可在转场动画发生的同时并行执行其他的动画,其作用与其说协调不如说辅助,主要在 Modal 转场和交互转场取消时使用,其他时候很少用到;遵守
总结下,5个协议只需要我们操心3个;实现一个最低限度可用的转场动画,我们只需要提供上面五个组件里的两个:转场代理和动画控制器即可,还有一个转场环境是必需的,不过这由系统提供;当进一步实现交互转场时,还需要我们提供交互控制器,也有现成的类供我们使用。
特殊的 Modal 转场
容器类 VC 的转场里 fromView
和 toView
是 containerView
的子层次的视图,而 Modal 转场里 presentingView
与 containerView
是同层次的视图,只有 presentedView
是 containerView
的子层次视图。
iOS 8引入的UIPresentationController
UIPresentationController
类,该类接管了 UIViewController
的显示过程,为其提供转场和视图管理支持。在 iOS 8.0 以上的系统里,你可以在 presentation
转场结束后打印视图控制器的结构,会发现 presentedVC
是由一个UIPresentationController
对象来显示的,查看视图结构也能看到 presentedView
是 UIView
私有子类的UITtansitionView
的子视图,这就是前面 containerView
的真面目.
当UIViewController
的modalPresentationStyle
属性为.Custom
时(不支持.FullScreen),我们有机会通过控制器的转场代理提供UIPresentationController
的子类对 Modal 转场
进行进一步的定制。实际上该类也可以在.FullScreen
模式下使用,但是会丢失由该类负责的动画,保险起见还是遵循官方的建议,只在.Custom
模式下使用该类。
UIPresentationController
类赋予 Modal 转场以下特性:
- 定制
presentedView
的外观,尺寸以及在containerView
中添加自定义视图并为这些视图添加动画; - 可以选择是否移除
presentingView
- 可以在不需要动画控制器的情况下单独工作
- iOS 8 中的自适应适应性布局
UIPresentationController
类提供了如下的方法参与转场,对转场过程实现了更加细致的控制,从命名便可以看出与动画控制器里的animateTransition:
的关系: {% codeblock swift lang:swift %} func presentationTransitionWillBegin() func presentationTransitionDidEnd(_ completed: Bool) func dismissalTransitionWillBegin() func dismissalTransitionDidEnd(_ completed: Bool) {% endcodeblock %} 除了 presentingView,UIPresentationController类拥有转场过程中剩下的角色: {% codeblock swift lang:swift %} //指定初始化方法。 init(presentedViewController presentedViewController: UIViewController, presentingViewController presentingViewController: UIViewController) var presentingViewController: UIViewController { get } var presentedViewController: UIViewController { get } var containerView: UIView? { get } //提供给动画控制器使用的视图,默认返回 presentedVC.view,通过重写该方法返回其他视图,但一定要是 presentedVC.view 的上层视图。 func presentedView() -> UIView?
{% endcodeblock %} 没有presentingView
是因为Custom
模式下presentingView
不受containerView
管理,UIPresentationController
类并没有改变这一点。iOS 8 扩充了转场环境协议,可以通过viewForKey:
方便获取转场的视图,而该方法在Modal
转场中获取的是presentedView()
返回的视图。因此我们可以在子类中将presentedView
包装在其他视图后重写该方法返回包装后的视图当做presentedView
在动画控制器中使用。
定制presentedView
外观:重载size方法和frameOfPresentedViewInContainerView属性
重载存储属性:get方法返回登场页面的位置和大小 {% codeblock lang:swift %} override var frameOfPresentedViewInContainerView: CGRect { var presentViewFrame = CGRect.zero let containerBounds = containerView?.bounds //登场控制器内容页面的大小 presentViewFrame.size = size(forChildContentContainer: presentedViewController, withParentContainerSize: (containerBounds?.size)!) presentViewFrame.origin.x = (containerBounds?.size.width)! - presentViewFrame.size.width return presentViewFrame }
//返回登场控制器内容页面的大小,在这里设置为屏幕宽度的三分之一款 override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { return CGSize.init(width:CGFloat(floorf(Float(parentSize.width/3.0))), height: parentSize.height) } {% endcodeblock %}
过渡动画,转场协调器(Transition Coordinator)
参与角色都准备好了,但有个问题,无法直接访问动画控制器,不知道转场的持续时间,怎么与转场过程同步?这时候前面提到的用处甚少的转场协调器(Transition Coordinator)将在这里派上用场。该对象可通过 UIViewController
的transitionCoordinator()
方法获取,这是 iOS 7 为自定义转场新增的 API,该方法只在控制器处于转场过程中才返回一个与当前转场有关的有效对象,其他时候返回 nil
。
转场开始
- 在
containerView
中插入过渡视图chromeView
- 为转场中
chromeView
过渡视图添加转场动画 presentedViewController.transitionCoordinator
转场协调器,添加转场的登场和退场动画 {% codeblock presentationTransitionWillBegin lang:swift %} override func presentationTransitionWillBegin() { chromeView.frame = (self.containerView?.bounds)! chromeView.alpha = 0.0 //在containerView
中插入视图chromeView
containerView?.insertSubview(chromeView, at:0) //coordinator转场协调器负责转场动画的呈现和dismissal let coordinator = presentedViewController.transitionCoordinator if (coordinator != nil) { //添加登场动画 coordinator!.animate(alongsideTransition: { (context:UIViewControllerTransitionCoordinatorContext!) -> Void in //animate the alpha to 1.0. self.chromeView.alpha = 1.0 }, completion:nil) } else { chromeView.alpha = 1.0 } } {% endcodeblock %}
转场结束
在presentedViewController.transitionCoordinator
转场协调器中添加转场的退场动画
{% codeblock dismissalTransitionWillBegin lang:swift %}
override func dismissalTransitionWillBegin()
{
let coordinator = presentedViewController.transitionCoordinator
if (coordinator != nil)
{
//添加退场动画
coordinator!.animate(alongsideTransition: {
(context:UIViewControllerTransitionCoordinatorContext!) -> Void in
self.chromeView.alpha = 0.0
}, completion:nil)
}
else
{
chromeView.alpha = 0.0
}
}
{% endcodeblock %}
适配屏幕旋转
在设备旋转的情况下,重置背景视图的外观和登场控制器内容的外观 {% codeblock lang:swift %} override func containerViewWillLayoutSubviews() { chromeView.frame = (containerView?.bounds)! presentedView?.frame = frameOfPresentedViewInContainerView } {% endcodeblock %}
Modal的两种PresentationStyle
- 设置整个转场动画是否将覆盖全屏幕
.OverFullScreen
: 浮动式全屏,即:登场视图下方的视图不会完全被遮挡.FullScreen
: 全覆盖全屏 即:占据全屏来显示登场视图 {% codeblock lang:swift %} //设置整个转场动画是否将覆盖全屏幕 override var shouldPresentInFullscreen: Bool { return true }
override var adaptivePresentationStyle: UIModalPresentationStyle { return UIModalPresentationStyle.fullScreen } {% endcodeblock %}
交互式转场
实现交互化
在非交互转场的基础上将之交互化需要两个条件:
由转场代理提供交互控制器,这是一个遵守UIPercentDrivenInteractiveTransition
供我们使用。我们不需要做任何配置,仅仅在转场代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。
交互控制器还需要交互手段的配合,最常见的是使用手势,或是其他事件,来驱动整个转场进程。
使用一个变量来标记交互状态配合转场交互
如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束。
在两个容器控制器NavigationController
和TabBarController
转场为例:
- 在
NavigationController
中点击NavigationBar
也能实现pop
返回操作,但此时没有了交互手段的支持,转场过程卡壳; - 在
TabBarController
的代理里提供交互控制器存在同样的问题,点击TabBar
切换页面时也没有实现交互控制。因此仅在确实处于交互状态时才提供交互控制器,可以使用一个变量来标记交互状态,该变量由交互手势来更新状态。
转场动画控制器:向转场中添加视图,执行转场动画
转场 API 是协议的好处是不受限于具体的类,只要对象实现该协议便能参与转场过程,这也带来另外一个好处:封装便于复用,尽管三大转场代理协议的方法不尽相同。
但它们返回的动画控制器遵守的是同一个协议,因此可以将动画控制器封装作为第三方动画控制器在其他控制器的转场过程中使用。
UIViewControllerAnimatedTransitioning
代理协议方法,提供了转场所需要的重要数据:
containerView()
:运行转场动画的容器视图- 转场视图控制器
- 方法一:
viewController(forKey:)
:UITransitionContextViewControllerKey
枚举值:from
,to
- 方法二:
viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0)
:iOS 8新增 API 用于方便获取参与转场的视图.两个键值:UITransitionContextFromViewKey
,UITransitionContextToViewKey
.
- 方法一:
{% codeblock lang:swift %} class ExampleAnimatedTransitioning: NSObject,UIViewControllerAnimatedTransitioning { //used to determine if the presentation animation is presenting (as opposed to dismissing). var isPresentation : Bool = false
//returns the duration in seconds of the transition animation.
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
{
//返回动画时间
return 0.5
}
//get the respective views of these view controllers.
//Next we get the container view and if the presentation animation is presenting, we add the to view to the container view.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
//get the from and to view controllers from the UIViewControllerContextTransitioning object.
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
//determine the start and end positions of the view.
let fromView = fromVC?.view
let toView = toVC?.view
let containerView = transitionContext.containerView
if isPresentation
{
containerView.addSubview(toView!)
}
//decide on which view controller to animate based on whether the transition is a presentation or dismissal
let animatingVC = isPresentation ? toVC : fromVC
let animatingView = animatingVC?.view
let finalFrameForVC = transitionContext.finalFrame(for: animatingVC!)
var initialFrameForVC = finalFrameForVC
//This will animate the view from right to left during a presentation and vice versa during dismissal.
initialFrameForVC.origin.x += initialFrameForVC.size.width
let initialFrame = isPresentation ? initialFrameForVC : finalFrameForVC
let finalFrame = isPresentation ? finalFrameForVC : initialFrameForVC
animatingView?.frame = initialFrame
//根据协议中的方法获取动画的时间。
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration, delay:0, usingSpringWithDamping:300.0, initialSpringVelocity:5.0, options:UIViewAnimationOptions.allowUserInteraction, animations:{
//we move the view to the final position.
animatingView?.frame = finalFrame
}, completion:{ (value: Bool) in
if !self.isPresentation {
//If the transition is a dismissal, we remove the view.
fromView?.removeFromSuperview()
}
//we complete the transition by calling transitionContext.completeTransition()
transitionContext.completeTransition(true)
})
}
// UIView.transitionFromView(fromView, toView: toView, duration: durantion, options: .TransitionCurlDown, completion: { _ in
// let isCancelled = transitionContext.transitionWasCancelled()
// transitionContext.completeTransition(!isCancelled)
// })
//如果实现了,会在转场动画结束后调用,可以执行一些收尾工作。
func animationEnded(_ transitionCompleted: Bool) {
//
}
} {% endcodeblock %}
转场代理协议(Transition Delegate)
自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。
实现转场代理协议方法,整合动画控制器和自定义展示控制器
- 返回管理用户信息视图控制器如何展示的控制器。前面实现的
ExamplePresentationViewController
类可同时处理presentation
转场 和dismissal
转场。 - 动画控制器为
presentation
和dismissal
转场分别提供了动画控制器。
UIPresentationController
只在 iOS 8中可用,通过available关键字可以解决 API 的版本差异。
{% codeblock lang:swift %} class ExampleTransitioningDelegate: NSObject,UIViewControllerTransitioningDelegate { //returns a presentation controller that manages the presentation of a view controller. func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { //presentation动画控制器 let presentationController = ExamplePresentationViewController(presentedViewController:presented, presenting:presenting)
return presentationController
}
//为presentation转场提供登场转场动画控制器
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
//登场转场动画控制器
let animator = ExampleAnimatedTransitioning()
animator.isPresentation = true
return animator
}
//为dismissal 转场提供退场转场动画控制器
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
//退场转场动画控制器
let animator = ExampleAnimatedTransitioning()
animator.isPresentation = false
return animator
}
} {% endcodeblock %}
使用自定义的转场的代理
自定义转场的第一步便是提供转场代理,告诉系统使用我们提供的代理而不是系统的默认代理来执行转场。 UIViewControllerTransitioningDelegate转场代理:
- 强引用代理变量:强引用的变量来维护该代理
- Modal转场代理的特性:由presentedVC自身来遵循转场代理
presentedVC.modalPresentationStyle
,和前两个容器控制器转场代理不同。 - 两种支持自定义转场模式:
.Custom
或.FullScreen
,默认值为.FullScreen
{% codeblock fromVC.class lang:swift %} //强引用的变量来维护该代理 let exampleTransitionDelegate = ExampleTransitioningDelegate() //create an instance of ExampleViewController which will provide the content to display. let presentedVC = ExampleViewController() presentedVC.modalPresentationStyle = .custom presentedVC.transitioningDelegate = exampleTransitionDelegate
//present this view controller. present(toVC, animated: true, completion: nil) {% endcodeblock %}
两种常规的转场方式
UIView方式:transitionFromView
不需要获取 containerView
以及手动添加 toView
就能实现一个指定类型的转场动画,而缺点则是只能使用指定类型的动画。
{% codeblock lang:swift %}
UIView.transitionFromView(fromView, toView: toView, duration: durantion, options: .TransitionCurlDown, completion: { _ in
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
{% endcodeblock %}
UIViewController方式:在子 VC 间转换的方法
该方法用 toVC 的视图转换 fromVC 的视图在父视图中的位置,并且执行animations闭包
里的动画。
{% codeblock lang:swift %}
transitionFromViewController:toViewController:duration:options:animations:completion:
{% endcodeblock %}
该方法仅限于在自定义容器控制器里使用,如果直接使用 UINavigationController
和 UITabBarController
调用该方法执行子VC间转换会抛出异常。
不过 iOS 7 中这两个容器控制器开放的自定义转场做的是同样的事情,回头再看第一章 Transition 解释,转场协议 API 将这个方法拆分成了上面的几个组件,并且加入了激动人心的交互控制,以便我们能够方便定制转场动画。
文章作者 iTBoyer
上次更新 2017-02-17