又過去十天了阳液,更新的速度有點慢誓酒,很不好意思诗轻,自己并不是一個高產(chǎn)的程序員嗅义,以后一定要加油了个榕。
上篇文章 Implementing a Container View Controller 翻譯+自我實踐 中解釋了如何實現(xiàn)一個簡單的容器類視圖控制器,這篇文章講講自定義容器類視圖控制器如何實現(xiàn)子視圖控制器的轉場切換的芥喇。
按照慣例,還是說一些基礎知識凰萨,iOS中的轉場继控,指的是視圖控制器的轉場。
官方文檔對Transition(轉場)有專門的章節(jié)進行介紹胖眷,希望初學的開發(fā)者仔細閱讀武通,理解一些基本的概念和原理:
The Presentation and Transition
轉場其實就是使得ViewController顯示在屏幕上,一共有兩種方式:
- present 一個視圖控制器珊搀,在日常開發(fā)中冶忱,主要體現(xiàn)在使用一個ViewController 模態(tài)跳轉到另外一個ViewController
- 在ContainerViewController 中顯示一個視圖控制器,這種體現(xiàn)在navigationController 和 tabbarController 等容器類視圖控制器中切換視圖控制器境析,例如導航的push 與 tabbarController的select 一個視圖控制器囚枪,當然我們自定義的ContainerController中提供的轉場也是屬于此類。
一般的開發(fā)者有時候只會簡單的使用第一種和第二種中系統(tǒng)提供的容器類視圖控制器的轉場劳淆,對轉場的原來不是很理解链沼,我們要實現(xiàn)一個自定義容器類視圖控制器的轉場,必須要把這些都講明白沛鸵。其實按照我個人的理解括勺,視圖控制器的轉場跟我們自己實現(xiàn)一兩個View之間動畫切換差不多,無非就是ViewController的View在切換曲掰。當然疾捍,幾個View的切換要在同一個父視圖上進行,那ViewController的View的切換則要在上篇文章中提到的ContainerView上進行栏妖,并且需要在轉場前和轉場后處理好子視圖控制器與容器類視圖控制器的父子關系的創(chuàng)建與管理乱豆,也就是調用addChildViewController:
、didMoveToParentViewController:
底哥、willMoveToParentViewController:
咙鞍、removeFromParentViewController
使得視圖控制器的生命周期函數(shù)在轉場的過程中正確的調用,并且在轉場過程中趾徽,完成視圖控制器的View在ContainerView上的布局续滋。在上面第一種提到的轉場和第二種中系統(tǒng)提供的容器類視圖控制器(navigationController和tabbarController等)的轉場,系統(tǒng)完成了一些操作孵奶,使得視圖控制器的生命周期函數(shù)得以正確調用疲酌,并且在containerView上正確的呈現(xiàn)。
在上篇文章中的例子里,我們在左右切換的方法中朗恳,調用了創(chuàng)建視圖控制器的父子關系的一些函數(shù)湿颅,并且調用了transitionFromViewController(fromViewController: UIViewController, toViewController: UIViewController, duration: NSTimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?)
來完成視圖控制器的View在ContainerView上的布局和動畫切換,該函數(shù)是iOS5中提供的粥诫,在當時用于自定義視圖控制器轉場的時候來完成動畫的轉場油航,例子里的核心代碼如下:
fromViewController.willMoveToParentViewController(nil)
self.addChildViewController(toViewController)
toViewController.view.bounds = container.bounds
let endCenter:CGPoint;
fromViewController.transitioningDelegate = self;
if orientation == Orientation.Left{
toViewController.view.center = CGPointMake(self.view.bounds.size.width + toViewController.view.bounds.size.width/2, self.view.bounds.size.height/2)
endCenter = CGPointMake(-self.view.bounds.size.width - toViewController.view.bounds.size.width/2, self.view.bounds.size.height/2)
}
else
{
toViewController.view.center = CGPointMake(-self.view.bounds.size.width - toViewController.view.bounds.size.width/2, self.view.bounds.size.height/2)
endCenter = CGPointMake(self.view.bounds.size.width + toViewController.view.bounds.size.width/2, self.view.bounds.size.height/2)
}
self.transitionFromViewController(fromViewController, toViewController: toViewController, duration: 0.25, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
toViewController.view.frame = fromViewController.view.frame
fromViewController.view.center = endCenter
}) { (completion) -> Void in
toViewController.didMoveToParentViewController(self)
fromViewController.removeFromParentViewController()
}
其實我們也可以不調用
transitionFromViewController: `這個方法,自己書寫視圖控制器View在ContainerView上的添加怀浆、刪除和布局谊囚,但一定要注意做這些事情的順序,以保證視圖控制器的生命周期方法正常調用执赡,雖然這是復雜的镰踏,但是確實可行的。
等到iOS7之后沙合,蘋果為了滿足開發(fā)者在這方面的需求奠伪,將轉場的過程進行了協(xié)議化,提出了如下幾個概念:
1.動畫控制器 (Animation Controllers) 遵從 UIViewControllerAnimatedTransitioning 協(xié)議首懈,用來專門處理轉場中的動畫切換
2.交互控制器 (Interaction Controllers) 通過遵從 UIViewControllerInteractiveTransitioning 協(xié)議绊率,用來處理可交互的轉場邏輯
3.轉場代理 (Transitioning Delegates) 遵守UIViewControllerTransitioningDelegate協(xié)議,在代理方法中究履,針對不同類型(模態(tài)跳轉還是導航push即舌,是可交互的還是不可交互)轉場,為轉場提供對應的動畫控制器或者是交互控制器挎袜。
4.轉場上下文 (Transitioning Contexts) 定義了轉場時需要的元數(shù)據(jù)顽聂,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵從 UIViewControllerContextTransitioning 協(xié)議盯仪,并且這是由系統(tǒng)負責生成和提供的紊搪。
轉場協(xié)調器(Transition Coordinators) 可以在運行轉場動畫時,并行的運行其他動畫全景。 轉場協(xié)調器遵從 UIViewControllerTransitionCoordinator 協(xié)議耀石。
以上幾個概念,都是使用定義在UIViewController 文件里的幾組協(xié)議爸黄,利用這幾種概念滞伟,我們通過協(xié)議能更好的將轉場的邏輯合理的進行分離,使得耦合度降低并且使得每一塊都專注于本身要處理的邏輯之中炕贵,并且能夠隨意復用梆奈,比方說,相同的轉場上下文中称开,我可以在轉場代理中提供不同的動畫控制器或交互控制器實現(xiàn)不同的轉場亩钟。同樣乓梨,我們可以使用如上概念,來自定義模態(tài)跳轉或者是navigationcontroller和tabbarcontroller的轉場清酥,只需要設置好轉場代理扶镀,在代理中提供不同的動畫控制器和交互控制器,而轉場上下文焰轻,在系統(tǒng)控件的轉場中由系統(tǒng)生成臭觉,提供給動畫或者交互控制器來使用。
以上的流程我們可以總結為:
1.無論是在任何類型下的轉場將要發(fā)生的時候辱志,我們在此之前胧谈,需要設置轉場的代理,例如在模態(tài)跳轉之前我們需要使用發(fā)起模態(tài)跳轉的視圖控制器設置transitioningDelegate來提供轉場代理荸频。
2.在UIViewControllerTransitioningDelegate 代理方法中提供
//視圖控制器消失的時候,需要提供的動畫控制器
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
//視圖控制器顯示的時候客冈,需要提供的動畫控制器
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
//可交互的轉場中旭从,視圖控制器消失的時候,需要提供的動畫控制器
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return nil
}
//可交互的轉場中场仲,視圖控制器顯現(xiàn)的時候和悦,需要提供的動畫控制器
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return nil
}`
以上適用于模態(tài)跳轉的流程,navigationController的自定義轉場渠缕,需要設置的導航視圖控制器的delegate(遵守UINavigationControllerDelegate)鸽素,在代理方法中如上第二部提供不同的動畫控制器和交互控制器,在此就不一一贅述了亦鳞。
那么我們在回過頭來馍忽,看看我們自定義的容器類視圖控制器如何實現(xiàn)轉場,前面已經(jīng)提到燕差,我們可以自己調用構建父子關系的函數(shù)并且操作ContainerView來做動畫轉場遭笋,或者借助transitionFromViewController來完成,但這次我們使用動畫控制器來實現(xiàn)徒探,好處很明顯瓦呼,在上面已經(jīng)介紹過了。我將上篇文章中的Demo在github上做了一個分支测暗,來說明這個做法央串,我注釋掉左右切換處理的函數(shù)中老的實現(xiàn)方法,來自定義動畫控制器實現(xiàn)切換動畫碗啄,新Demo的地址如下:
ContainerViewController
在
func swipeFromViewController(fromViewController:UIViewController,ToViewController toViewController:UIViewController, WithOrientation orientation:Orientation)`中质和,我注釋掉之前的邏輯,新的邏輯如下:
//第一步稚字,在轉場之前侦另,構建fromViewController toViewController和containerViewController的父子關系
fromViewController.willMoveToParentViewController(nil)
self.addChildViewController(toViewController)
//第二步,因為我們是自定義的容器控制器,我們需要提供轉場上下文
let context = CustomTransitionContext(containerView: container, toViewController: toViewController, fromViewController: fromViewController)
//第三步褒傅,設置轉場完成之后弃锐,調用一個回調的閉包,來處理父子關系的重新構建
context.completeHandle = {
(isComplete : Bool) -> Void in
toViewController.didMoveToParentViewController(self)
fromViewController.removeFromParentViewController()
}
let animator = CustomTransitionAnimtor(context: context)
animator.orientaion = orientation
animator.animateTransition(context)`
接下來我們看一下我們自定義的轉場上下文殿托,一個遵守UIViewControllerContextTransitioning協(xié)議的NSObject對象:
class CustomTransitionContext: NSObject,
UIViewControllerContextTransitioning
{
weak var customContainerView : UIView?
private weak var toViewController:UIViewController?
private weak var fromViewController:UIViewController?
internal var completeHandle : ((isComplete : Bool)->Void)?;
var animating : Bool = true
func isAnimated() -> Bool {
return animating
}
func isInteractive() -> Bool {
return false
}
func transitionWasCancelled() -> Bool {
return false
}
func presentationStyle() -> UIModalPresentationStyle {
return UIModalPresentationStyle.Custom
}
//完成轉場之后要做的操作
func completeTransition(didComplete: Bool) {
if let handler = completeHandle {
animating = false
handler(isComplete: didComplete)
}
}
func updateInteractiveTransition(percentComplete: CGFloat) {
}
func finishInteractiveTransition() {
}
func cancelInteractiveTransition() {
}
//轉場上下文提供的UITransitionContextFromViewController與UITransitionContextToViewController
func viewControllerForKey(key: String) -> UIViewController? {
switch key{
case UITransitionContextFromViewControllerKey:
return fromViewController
case UITransitionContextToViewControllerKey:
return toViewController
default:
return nil
}
}
@available(iOS 8.0,*)
func viewForKey(key: String) -> UIView? {
switch key{
case UITransitionContextFromViewKey:
return fromViewController?.view
case UITransitionContextToViewKey:
return toViewController?.view
default:
return nil
}
}
func finalFrameForViewController(vc: UIViewController) -> CGRect {
return CGRectZero
}
func initialFrameForViewController(vc: UIViewController) -> CGRect {
return CGRectZero
}
func containerView() -> UIView? {
return self.customContainerView
}
//提供一個便利構造方法霹菊,能夠獲取轉場相關的視圖控制器與ContainerView
convenience init(containerView : UIView? ,toViewController : UIViewController , fromViewController : UIViewController){
self.init()
self.toViewController = toViewController
self.fromViewController = fromViewController
self.customContainerView = containerView
}
func targetTransform() -> CGAffineTransform {
return CGAffineTransformIdentity
}
}
以上就是轉場上下文的構建,帶有注釋的方法是比較核心的幾個方法支竹,而其他的都是為了滿足協(xié)議而補全的方法旋廷。核心就是使得我們自定義的轉場上下文擁有ContainerView和要切換的視圖控制器,提供給動畫控制器使用礼搁。
下面是重點饶碘,動畫控制器的實現(xiàn):
class CustomTransitionAnimtor: NSObject , UIViewControllerAnimatedTransitioning {
internal var orientaion : ContainerViewController.Orientation = ContainerViewController.Orientation.Left
var toViewController : UIViewController?
var fromViewController : UIViewController?
var privateContext : CustomTransitionContext;
//該方法提供轉場動畫需要的時間
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.25
}
//自定義的構造函數(shù)用來獲取自定義的轉場上下文
init(context : CustomTransitionContext){
privateContext = context
super.init()
}
//該方法是用來處理動畫邏輯的
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//獲取containerView的引用
let containerView = privateContext.containerView()
//獲取要轉場的相關視圖控制器,通過自定義的轉場上下文來獲取
let toViewController = privateContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let fromViewController = privateContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
//配置相關控制器的View的信息馒吴,大小與在Container的添加和刪除
toViewController?.view.bounds = (fromViewController?.view.bounds)!
var endCenter : CGPoint;
if orientaion == ContainerViewController.Orientation.Left{
endCenter = CGPointMake(-(fromViewController?.view.bounds.size.width)!, (containerView?.bounds.size.height)!/2)
toViewController?.view.center = CGPointMake((containerView?.bounds.size.width)! + (toViewController?.view.bounds.width)!/2, (containerView?.bounds.height)!/2)
}
else
{
endCenter = CGPointMake((containerView?.bounds.size.width)! + (fromViewController?.view.bounds.size.width)!/2, (containerView?.bounds.size.height)!/2)
toViewController?.view.center = CGPointMake(-(containerView?.bounds.size.width)! - (toViewController?.view.bounds.width)!/2, (containerView?.bounds.height)!/2)
}
containerView?.addSubview((toViewController?.view)!)
//按照預定的來實現(xiàn)動畫切換UIView.animateWithDuration(self.transitionDuration(privateContext), animations: { () -> Void in
toViewController?.view.center = (fromViewController?.view.center)!
fromViewController?.view.center = endCenter
}) { (isComplection) -> Void in
fromViewController?.view.removeFromSuperview()
self.animationEnded(true)
}
}
//在完成轉場動畫的時候扎运,調用自定義轉場上下文的completeTransition方法,來告訴上下文轉場已經(jīng)完成
func animationEnded(transitionCompleted: Bool) {
self.privateContext.completeTransition(true)
}
}
以上我們清晰的看到饮戳,動畫控制器有三個方法豪治,依次是提供動畫時間,動畫過程和動畫完成后要做的一些邏輯操作扯罐,非常簡單负拟,動畫控制器就是專一完成切換的動畫的邏輯的,代碼注釋已經(jīng)相當清楚歹河。
以上的步驟很清晰掩浙,但有兩個問題,那就是秸歧,為什么我們沒有使用轉場代理涣脚,在轉場代理的協(xié)議方法中提動畫控制器?第二是為什么我們需要自己構建轉場上下文寥茫?其實這個也是困擾我的問題遣蚀,其實這兩個問題是一體的,如果設置了模態(tài)跳轉的轉場代理纱耻,我們在動畫控制器里獲取的是系統(tǒng)構建的轉場上下文芭梯,但經(jīng)過我的實驗,很明顯系統(tǒng)提供的轉場上下文弄喘,通過viewControllerForKey提取出來的相關控制器不是正確的玖喘,也就是說,我們在容器類視圖控制器中使用模態(tài)跳轉的方式來自定義模態(tài)跳轉的轉場動畫是不可行的蘑志,因為模態(tài)跳轉構建的轉場上下文累奈,fromViewController一直為容器視圖控制器而不是真正的一個子視圖控制器贬派。所以,我們要自定義轉場上下文澎媒,來通過各種協(xié)議完成整個轉場過程搞乏。
Ok,以上就是一個使用動畫控制器和自定義轉場上下文來實現(xiàn)的自定義容器類視圖控制器里子視圖控制器的簡單切換戒努,下個目標请敦,是實現(xiàn)更為復雜的可交互的容器控制器的轉場切換,在此感謝大家的閱讀储玫!