ContainerViewController的ViewController 轉場

又過去十天了,更新的速度有點慢,很不好意思,自己并不是一個高產的程序員,以后一定要加油了。
上篇文章 Implementing a Container View Controller 翻譯+自我實踐 中解釋了如何實現一個簡單的容器類視圖控制器,這篇文章講講自定義容器類視圖控制器如何實現子視圖控制器的轉場切換的。

按照慣例,還是說一些基礎知識,iOS中的轉場,指的是視圖控制器的轉場。
官方文檔對Transition(轉場)有專門的章節進行介紹,希望初學的開發者仔細閱讀,理解一些基本的概念和原理:
The Presentation and Transition

轉場其實就是使得ViewController顯示在屏幕上,一共有兩種方式:

  1. present 一個視圖控制器,在日常開發中,主要體現在使用一個ViewController 模態跳轉到另外一個ViewController
  1. 在ContainerViewController 中顯示一個視圖控制器,這種體現在navigationController 和 tabbarController 等容器類視圖控制器中切換視圖控制器,例如導航的push 與 tabbarController的select 一個視圖控制器,當然我們自定義的ContainerController中提供的轉場也是屬于此類。

一般的開發者有時候只會簡單的使用第一種和第二種中系統提供的容器類視圖控制器的轉場,對轉場的原來不是很理解,我們要實現一個自定義容器類視圖控制器的轉場,必須要把這些都講明白。其實按照我個人的理解,視圖控制器的轉場跟我們自己實現一兩個View之間動畫切換差不多,無非就是ViewController的View在切換。當然,幾個View的切換要在同一個父視圖上進行,那ViewController的View的切換則要在上篇文章中提到的ContainerView上進行,并且需要在轉場前和轉場后處理好子視圖控制器與容器類視圖控制器的父子關系的創建與管理,也就是調用addChildViewController:didMoveToParentViewController:willMoveToParentViewController:removeFromParentViewController 使得視圖控制器的生命周期函數在轉場的過程中正確的調用,并且在轉場過程中,完成視圖控制器的View在ContainerView上的布局。在上面第一種提到的轉場和第二種中系統提供的容器類視圖控制器(navigationController和tabbarController等)的轉場,系統完成了一些操作,使得視圖控制器的生命周期函數得以正確調用,并且在containerView上正確的呈現。

在上篇文章中的例子里,我們在左右切換的方法中,調用了創建視圖控制器的父子關系的一些函數,并且調用了transitionFromViewController(fromViewController: UIViewController, toViewController: UIViewController, duration: NSTimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?)來完成視圖控制器的View在ContainerView上的布局和動畫切換,該函數是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之后,蘋果為了滿足開發者在這方面的需求,將轉場的過程進行了協議化,提出了如下幾個概念:

1.動畫控制器 (Animation Controllers) 遵從 UIViewControllerAnimatedTransitioning 協議,用來專門處理轉場中的動畫切換
2.交互控制器 (Interaction Controllers) 通過遵從 UIViewControllerInteractiveTransitioning 協議,用來處理可交互的轉場邏輯
3.轉場代理 (Transitioning Delegates) 遵守UIViewControllerTransitioningDelegate協議,在代理方法中,針對不同類型(模態跳轉還是導航push,是可交互的還是不可交互)轉場,為轉場提供對應的動畫控制器或者是交互控制器。
4.轉場上下文 (Transitioning Contexts) 定義了轉場時需要的元數據,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵從 UIViewControllerContextTransitioning 協議,并且這是由系統負責生成和提供的。
轉場協調器(Transition Coordinators) 可以在運行轉場動畫時,并行的運行其他動畫。 轉場協調器遵從 UIViewControllerTransitionCoordinator 協議。

以上幾個概念,都是使用定義在UIViewController 文件里的幾組協議,利用這幾種概念,我們通過協議能更好的將轉場的邏輯合理的進行分離,使得耦合度降低并且使得每一塊都專注于本身要處理的邏輯之中,并且能夠隨意復用,比方說,相同的轉場上下文中,我可以在轉場代理中提供不同的動畫控制器或交互控制器實現不同的轉場。同樣,我們可以使用如上概念,來自定義模態跳轉或者是navigationcontroller和tabbarcontroller的轉場,只需要設置好轉場代理,在代理中提供不同的動畫控制器和交互控制器,而轉場上下文,在系統控件的轉場中由系統生成,提供給動畫或者交互控制器來使用。
以上的流程我們可以總結為:

  1.無論是在任何類型下的轉場將要發生的時候,我們在此之前,需要設置轉場的代理,例如在模態跳轉之前我們需要使用發起模態跳轉的視圖控制器設置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
}
 //可交互的轉場中,視圖控制器顯現的時候,需要提供的動畫控制器
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return nil
}`

以上適用于模態跳轉的流程,navigationController的自定義轉場,需要設置的導航視圖控制器的delegate(遵守UINavigationControllerDelegate),在代理方法中如上第二部提供不同的動畫控制器和交互控制器,在此就不一一贅述了。

那么我們在回過頭來,看看我們自定義的容器類視圖控制器如何實現轉場,前面已經提到,我們可以自己調用構建父子關系的函數并且操作ContainerView來做動畫轉場,或者借助transitionFromViewController來完成,但這次我們使用動畫控制器來實現,好處很明顯,在上面已經介紹過了。我將上篇文章中的Demo在github上做了一個分支,來說明這個做法,我注釋掉左右切換處理的函數中老的實現方法,來自定義動畫控制器實現切換動畫,新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協議的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
  }

    }

以上就是轉場上下文的構建,帶有注釋的方法是比較核心的幾個方法,而其他的都是為了滿足協議而補全的方法。核心就是使得我們自定義的轉場上下文擁有ContainerView和要切換的視圖控制器,提供給動畫控制器使用。
下面是重點,動畫控制器的實現:

   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
}
//自定義的構造函數用來獲取自定義的轉場上下文
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)!)

            //按照預定的來實現動畫切換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方法,來告訴上下文轉場已經完成
func animationEnded(transitionCompleted: Bool) {
    self.privateContext.completeTransition(true)
}

}
以上我們清晰的看到,動畫控制器有三個方法,依次是提供動畫時間,動畫過程和動畫完成后要做的一些邏輯操作,非常簡單,動畫控制器就是專一完成切換的動畫的邏輯的,代碼注釋已經相當清楚。
以上的步驟很清晰,但有兩個問題,那就是,為什么我們沒有使用轉場代理,在轉場代理的協議方法中提動畫控制器?第二是為什么我們需要自己構建轉場上下文?其實這個也是困擾我的問題,其實這兩個問題是一體的,如果設置了模態跳轉的轉場代理,我們在動畫控制器里獲取的是系統構建的轉場上下文,但經過我的實驗,很明顯系統提供的轉場上下文,通過viewControllerForKey提取出來的相關控制器不是正確的,也就是說,我們在容器類視圖控制器中使用模態跳轉的方式來自定義模態跳轉的轉場動畫是不可行的,因為模態跳轉構建的轉場上下文,fromViewController一直為容器視圖控制器而不是真正的一個子視圖控制器。所以,我們要自定義轉場上下文,來通過各種協議完成整個轉場過程。

Ok,以上就是一個使用動畫控制器和自定義轉場上下文來實現的自定義容器類視圖控制器里子視圖控制器的簡單切換,下個目標,是實現更為復雜的可交互的容器控制器的轉場切換,在此感謝大家的閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容