自定義 push 和 pop 實現相冊翻開效果(上)

效果預覽:

AlbumTransition.gif

前言

蘋果自家應用 Photos 里點擊相冊后的動畫是非常精妙的,而且是可交互的。我有類似的動畫需求,上面是我自己的設計效果。本指南分上下兩篇,分別探討非交互和交互動畫的實現。

本文是將三個月前的 Demo 重構后重新寫的,重構后,這個效果可以方便地在你的工程中使用,僅需添加幾行代碼和幾個簡單的設置。效果適用場景:兩個UICollectionViewController類之間的 push 和 pop 操作。Demo 是個小型的相冊瀏覽器, 這完全是基于我的需求來做的,因此在初期并沒有考慮做成一個手把手教你實現這個效果的教程,不過前面說了,僅需添加幾行代碼就可在你的工程里使用,花上幾分鐘搭建一個場景照著做下來也是沒問題的。另外,部分細節比較繁瑣,都放進文章里就太長了,想了解的話看源代碼,遇到這部分我會提示的。

Demo 地址:SDECollectionViewAlbumTransition。

我把 iOS 里的動畫分為兩種:趣味動畫和邏輯動畫,前者比如一些加載場景的動畫,用來消磨時間,怎么炫酷都可以,后者是符合場景變化的動畫,符合邏輯最重要,如果還能很有趣那就更好了。我實現的效果算得上符合邏輯,離有趣或者酷還有點距離。

如上所示,我希望呈現出打開相簿后照片飛出來的效果,這個設計是行為上的擬物,最好翻開封面時還能發出金光,NO,NO,太浮夸了,簡直跟中華小當家或者國產奇幻劇開寶箱似的。當然,主要是我不知道怎么做,會做的話我就會做出來給大家看的,不過,我是不會把這種效果放在正常的產品里的,在游戲界這種效果比較常見,比如爐石里新卡牌點開時就帶這種圣光效果。

從技術上講,以 push 為例:圖片像一本相冊的封面一樣翻開,這是一個可用 transform 實現的 翻轉動畫;下一層級的視圖也就是相冊里的照片在封面后出現,這個效果需要縮小照片并按一定規則排列好;封面繼續往左翻動,而照片則移動到預定位置并在這個過程中恢復到原大小。這個動畫本質上就是個 View Controller Transition 加上多個元素協作進行動畫的過程??偟膩碚f,動畫分為兩個部分,首先是自定義 push 和 pop,其次是各種元素的協作?,F在先攻克第一個難點,下面進入科普時間。

View Controller Transition 視圖控制器轉換

對于這個話題,我推薦:1. WWDC13 上的 Custom Transitions Using View Controllers,2.Custom Transitions on iOS,3. Objc.io 的自定義 ViewController 容器轉場。以及一個自定義 transition 效果的庫:VCTransitionsLibrary,可以讀讀代碼看看這些效果怎么實現的。

自定義 transition 類型

View Controller Transition 是什么?其實平時你就一直能看到,在切換或是添加新的視圖控制器來顯示視圖的時候發生的過程就是 ViewController Transition,比如 push 或 pop 一個 View Controller,在 TabBarController 中切換到其他 View Controller,以模態方式顯示另外一個 View Controller。只不過,在 iOS 7 之前我們無法干涉這個過程,從 iOS 7 開始支持自定義 View Controller Transition,目前僅支持以下四種自定義類型:


iOS 支持的的自定義視圖轉換類型 from WWDC13 #218

除了最后一個是布局轉換,前三種基本囊括了 iOS 中顯示切換視圖的全部方式:
1.Modal 視圖的顯示和消失;
2.TabBar Controller 在子視圖中切換;
3.Navigation Controller 推入和推出視圖。

其中 presentations and dismissals 只支持 UIModalPresentationFullScreen 和 UIModalPresentationCustom 這兩種 Modal 視圖的顯示和消失。

文章開頭的效果是第三種,需要實現自定義 push 和 pop。

Transition Protocol

iOS 提供了幾套 protocol 來滿足自定義 transition 的需求。

WWDC13#218-Custom Transition 的構成

對以上 protocol 的解釋節選自 Objc.io 的自定義 ViewController 容器轉場

iOS 7 自定義視圖控制器轉場的 API 基本上都是以協議的方式提供的,這也使其可以非常靈活的使用,因為你可以很簡單地將它們插入到你的類中。最主要的五個組件如下:
1.動畫控制器 (Animation Controllers) 遵從UIViewControllerAnimatedTransitioning協議,并且負責實際執行動畫。
2.交互控制器 (Interaction Controllers) 通過遵從UIViewControllerInteractiveTransitioning協議來控制可交互式的轉場。
3.轉場代理 (Transitioning Delegates) 根據不同的轉場類型方便的提供需要的動畫控制器和交互控制器。
4.轉場上下文 (Transitioning Contexts) 定義了轉場時需要的元數據,比如在轉場過程中所參與的視圖控制器和視圖的相關屬性。 轉場上下文對象遵從UIViewControllerContextTransitioning協議,并且這是由系統負責生成和提供的。
5.轉場協調器(Transition Coordinators) 可以在運行轉場動畫時,并行的運行其他動畫。 轉場協調器遵從UIViewControllerTransitionCoordinator協議。

看暈了?沒關系。這五個組件并不是全部都需要你提供,實現一個最簡單的非交互的自定義 transition,只需要實現1和3即可,其實還會用到4,不過大部分情況下這個組件由系統提供給我們,我們只需要實現組件1和3就可以了。

實戰

準備工作

這篇不涉及交互過程,因此我單獨做了個分支:No-Interaction-Transition,是本篇內容的最終版本;或者你還是想自己動手,使用純色塊的 Cell 就好了,幾分鐘就能搞定,又或者不怕再麻煩一點,提取這個分支里面 Example 文件夾里的文件替換到你的工程好了。到這里還是很簡單的,如果覺得不簡單,那就看看好了,把本文加入待讀列表過一個月后再來學習。

Demo 里有三個分支,默認分支是能夠自動添加 pinch 手勢支持 pop 操作,還是就是這篇文章的分支 No-Interaction-Transition,還有一種就是同時支持 push 和 pop 操作的 pinch 手勢的分支 Pinch-Push-Pop-Transition。

下面需要你配置這樣的一個場景,在此基礎上逐步改造成最終的效果:在 storyboard 里放置一個UINavigationController和兩個UICollectionViewController,如果你不用 storyboard,相信你也能自己搞定設置。

使用場景

下面使用 fromVC 和 toVC 分別代表 push 和 pop 過程涉及的源和目標UICollectionViewController,animationController 代表動畫控制器,它執行真正的動畫。實現一個最基本的非自定義 push,在你的 fromVC 里實現以下代理方法:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
        /*對 toVC 做一些設置,然后 push*/
        ......
        self.navigationController?.pushViewController(toVC, animated: true)
    }
}

現在,一個最簡單的場景就搭建完成了。此時,push 和 pop 都是系統替我們完成,運行程序,動畫效果是 Slide。接下來,我們就把這個動畫換成我設計的。

如果你是在 storyboard 里通過拉 segue 來完成跳轉,那需要你去- prepareForSegue:sender:里做一些調整了,但先別這么干,按照我的節奏來。

接手系統 transition

第一步,為UINavigationController提供遵守UINavigationControllerDelegate協議的對象(組件3)作為代理 delegate,在 push 和 pop 時系統會要求這個 delegate 來提供動畫控制器和交互控制器;沒有提供這個代理時,比如上面的情況里,系統將會使用默認的 Slide 動畫。該協議的方法名很直白,其中前者必須實現,用于提供組件1來執行實際的動畫,后者提供組件2實現交互動畫,是可選的。
- navigationController:animationControllerForOperation:fromViewController:toViewController:
- navigationController:interactionControllerForAnimationController:

新建SDENavigationControllerDelegate類作為代理,聲明如下:

在 storyboard 里拖一個 NSObject 下面圖中這一塊區域,然后將其類設置為SDENavigationControllerDelegate。你沒看錯,就是拖一個 NSObject,在你經常拖控件的地方輸入 object 就能看到。如果你還不知道,恭喜,現在你又學到新知識了。

在 storyboard 里為 navigation controller 設置 delegate

小坑預警:如果你想在代碼里設置UINavigationController的 delegate,那么viewDidLoad()并不是一個合適的地方,因為此時 ViewController 尚未被推入UINavigationControllerviewControllers棧里,通過UIViewController.navigationController得到的只是 nil。哪兒合適,在viewDidAppear()后調用的方法都可以,這么說這有點......作為一個UICollectionViewController,push 時在 didSelectCell 那個方法里最合適了。

本文將只實現非交互的動畫,可交互的動畫在系列下篇討論。在SDENavigationControllerDelegate類里實現以下方法提供動畫控制器:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    //需要通過是 push 還是 pop 操作來執行不同的動畫,因此自定義了一個需要用操作類型來初始化的動畫控制器
    let animationController = SDEPushAndPopAnimationController(operation: operation)控制器
    return animationController
}

第二步,實現上面提供的動畫控制器類SDEPushAndPopAnimationController,該類遵守 UIViewControllerAnimatedTransitioning協議,需要實現以下方法:

- transitionDuration: //提供 transition animation 的持續時間
- animateTransition:  //執行動畫的地方,最重要的方法
- animationEnded:     //可選方法,動畫完畢后調用,大部分時候用不上

SDEPushAndPopAnimationController類的實現:

class SDEPushAndPopAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
//通過變量來保存操作類型
private var operation: UINavigationControllerOperation

init(operation: UINavigationControllerOperation){
    self.operation = operation
    super.init()
}

//返回動畫執行時間,實際上 navigationBar 的動畫時間也由該方法返回的時間決定。
//所有自定義的 navigationbar transition 的動畫效果都是 cross fade。
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
    return 1.0
}
//執行動畫的地方
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    switch operation{
    case .Push:
    /*do some thing*/
    case .Pop:
    /*do some thing also*/
    default: break
    }
}

WT...恩,暫時先這么處理吧。接下來,再次進入科普時間。

來看看 WWDC13 Session 218 中對 ViewController Transition 的解釋:


ViewController Transition 圖解

NavigationController 維持的 ViewController 的結構和我們想象的一樣,是個棧,但其對應的 View 的結構卻不是這樣。在 transition 結束時,fromView 被從 containerView 中被移除,如果我們沒有這么做,系統會替我們完成的。這么看來,containerView 里只保留棧頂 ViewController 的視圖,也就是屏幕上我們看到的那個視圖。

圖中的兩個狀態之間的變化就發生在動畫控制器的- animateTransition:方法里,不過動畫的執行不限于這里,viewWillXXX, viewDidXXX等這些方法里都可以執行你想要的動畫。不過,所有動畫放在這里執行還有一個最最最最最重要的目的,先放結論:你想納入交互化控制過程的動畫必須在- animateTransition:里執行,而且,必須使用 UIView Animation 來實現,不要使用 Core Animation,在系列下篇里實現交互動畫時會詳細討論有關細節??破战Y束,返回實現過程。

定制動畫

animateTransition:方法的原型為:

func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

該函數的參數也就是組件4,由系統提供給我們,它提供了 transition 過程中我們需要的絕大部分信息,包括參與 transition 過程的控制器以及 transition 過程的狀態,最后還要將 transition 的執行結果通知給系統。

animateTransition:方法中要做的事情主要是這樣:

func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
    //由系統提供的 transitionContext 能提供大部分需要的信息,下面的,應該很好理解吧。
    let containerView = transitionContext.containerView()
    let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as? UICollectionViewController
    let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as? UICollectionViewController
    let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
    let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
    let duration = transitionDuration(transitionContext)//這是要求實現的另外一個方法,往回看
        
    //containerView 在 transition 過程中擔任 fromView 和 toView的父視圖;將 toView 添加到 containerView 中,toView 才能顯示在屏幕上
    containerView?.addSubview(toView!)
    UIView.animateWithDuration(duration, animations: {
        /*添加動畫*/
    }, completion: { _ in
            //結束 transition 過程
            let isCancelled = transitionContext.transitionWasCancelled()
            transitionContext.completeTransition(!isCancelled)
    })
}

在很多文章里,會給你演示一些簡單的動畫,實際上,我們可以對當前視圖 fromView 和下一屏視圖 toView 做任何動畫,僅限于你的想象力以及實現能力。

VCTransitionsLibrary 這個庫包含了十種效果,都是針對視圖整體實現的動畫,而當 transition 涉及視圖中的子視圖時,這個庫就不適用了。比如神奇移動,就是將 fromView 上的子視圖移動到 toView 上,實現思路有兩種:一是,toView 出現時,將目標元素移動到源元素的位置進行遮擋,然后移動到預定位置,比較簡單;二是將 fromView 和 toView 中相同子視圖都隱藏,對該子視圖截圖并加入 toView 中作為偽裝,然后將偽裝的子視圖移動到 toView 上的指定位置,最后移除偽裝的子視圖然后將隱藏的子視圖恢復顯示。這兩個方法中很重要的一點就是無論是偽裝的還是真正的子視圖在開始和結束移動時的位置和大小都要吻合,不然就露餡了。

回到這個動畫,前面提到,實現交互動畫,一定要使用 UIView Animation 而不是 Core Animation。而且這里的動畫還涉及多個元素的配合,不同元素的動畫的開始時間與持續時間都不一樣,使用 UIView Animation 是沒法滿足這個要求的,因為常規的延遲執行手段在交互動畫里沒有作用,只有一個解決辦法:UIView key frame animation,這里 push 和 pop 過程中的動畫都是采用這種方式實現的。

UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
    //添加多步動畫
    self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
    self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
    self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
}, completion: { finished in
    let isCancelled = transitionContext.transitionWasCancelled()
    transitionContext.completeTransition(!isCancelled)
})

開頭的效果拆分成三個動畫完成:

1.翻開封面的動畫。由于 toView 里并沒有封面這個元素,需要使用偽裝的封面,push 時隱藏原封面的同時在 toView 上添加和原封面內容一樣的視圖來欺騙我們的眼睛,pop 時則將這個偽裝封面翻回去,然后恢復源封面的顯示。封面的第二個問題,如何保證封面在 toView 上依然保持在視覺正確的位置。這個也好解決,無論當前 collectionView 怎么移動,封面相對于 fromView.superView 和封面相對于 toView.superView 的位置是一樣的,因為這兩個位置都是相對于當前屏幕的位置。UIView 有一套"convertXXX"的方法用于屬于同一個 UIWindow 的視圖之間進行坐標的轉換:

//配合好封面上翻轉和消失動畫的時間
func addKeyFrameAnimationInPushForFakeCoverView(coverView: UIView?){
    //封面是最早執行動畫的元素,并且在整體動畫的中途完成。
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
        var flipLeftTransform = CATransform3DIdentity
        flipLeftTransform.m34 = -1.0 / 500.0
        flipLeftTransform = CATransform3DRotate(flipLeftTransform, CGFloat(-M_PI), 0.0, 1.0, 0.0)
        coverView?.layer.transform = flipLeftTransform
    })
}

2.調整 visibleCells 的動畫,這在 pop 時不是問題,但是在 push 時,你會發現在- animateTransition:里通過 toVC.collectionView?.visibleCells()返回的是空數組,沒法獲取 visibleCells 意味著我們沒法對即將出現的 visibleCells 進行調整,怎么辦?這個問題在三個月前將我折磨死了,可以從這篇記錄里看到當時的歷程,由于無法獲取 visibleCells 而苦苦尋求其他辦法最終卻失敗。解決辦法的關鍵是從這篇教程 How to Create an iOS Book Open Animation 里得知的,使用toVC.view.snapshotViewAfterScreenUpdates(true)能夠強制視圖立即進行刷新,此時可以獲取 visibleCells,事實上可以還有方法也可以:- layoutIfNeeded。具體對于這些 visibleCells 根據自身的 indexPath 來設置大小和位置是一件比較繁瑣的事情,這部分代碼放在setupVisibleCellsBeforePushToVC:里了,這里不詳細討論。

func addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC: UICollectionViewController){
    let collectionView = toVC.collectionView!
    for cell in collectionView.visibleCells(){
        //不同位置的 cell 的動畫的開始時間和持續時間有些許差別,讓離得中心越遠的元素越早到達位置,最后的效果非常賞心悅目。這個是從上面那個庫里學來的,但目前還有點瑕疵。
        let relativeStartTime = ......
        var relativeDuration =  ......
        //以漸顯的方式出現在封面后,但這個效果一般
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.7, animations: {
            cell.alpha = 1
        })
        //在封面完全翻開后才開始照片的動畫,開始時間各有差異。
        UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
            cell.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1)
        })
        UIView.addKeyframeWithRelativeStartTime(0.5 + relativeStartTime, relativeDuration: relativeDuration, animations: {
            cell.center = layoutAttributes!.center
        })
    }
}

3.調整視圖背景色。這是個很不起眼的小地方,但可能會讓你栽個大跟頭。如果你設置了 toVC 的視圖的背景色,動畫開始時屏幕就會呈現該背景,這時候 fromView 就立刻不可見了,動畫效果是非常糟糕的;這時候你或許會在 storyboard 里將 toVC 的 collectionView 的背景色調整為透明色來解決這個問題,可惜在動畫結束后,背景色突然變黑,這是因為動畫結束后,fromView 被移除出去了, toView 沒有了背景空無一物,屏幕背景自然就變成黑色了。解決辦法是,在 storyboard 里將 toVC 的 collectionView 的背景色設置為透明色,然后在 transition 過程中使用動畫來進行過渡到你需要的背景色。

func addkeyFrameAnimationForBackgroundColorInPush(fromVC: UICollectionViewController, toVC: UICollectionViewController){
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0, animations: {
        let toCollectionViewBackgroundColor = fromVC.collectionView?.backgroundColor
        toVC.collectionView?.backgroundColor = toCollectionViewBackgroundColor
    })
}

animateTransition:執行動畫之前,還有一個問題,pop 結束后要恢復被隱藏的封面,需要在 push 前保留這個被點擊的封面的 indexpath 以便在 pop 結束時能夠將之恢復。但又不想在UICollectionViewController添加屬性,因為你讓別人在自己的工程中為這個類添加這個屬性還是挺麻煩的,有辦法:extensition + associated object,這個技巧是從這個庫學來的。為UICollectionViewController添加一個 extension,為所有的UICollectionViewController類添加下面兩個屬性:

private var selectedIndexPathAssociationKey: UInt8 = 0
private var coverRectInSuperviewKey: UInt8 = 1

extension UICollectionViewController {
    //保存被選中的封面的索引
    var selectedIndexPath: NSIndexPath! {
        get {
            return objc_getAssociatedObject(self, &selectedIndexPathAssociationKey) as? NSIndexPath
        }
        set(newValue) {
            objc_setAssociatedObject(self, &selectedIndexPathAssociationKey, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    //記錄被選中的封面相對于屏幕的位置,這個會被傳遞給 toVC,以便于在 toVC 里調整 visibleCells 的位置和大小使之能夠隱藏在封面后面
    var coverRectInSuperview: CGRect! {
        get {
            let value = objc_getAssociatedObject(self, &coverRectInSuperviewKey) as? NSValue
            return value?.CGRectValue()
        }
        set(newValue){
            let value = NSValue(CGRect: newValue)
            objc_setAssociatedObject(self, &coverRectInSuperviewKey, value, .OBJC_ASSOCIATION_RETAIN)
        }
    }
} 

然后要在之前的代理方法里添加一行代碼:

override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath:NSIndexPath) {
    if let toVC = self.storyboard?.instantiateViewControllerWithIdentifier("XXX") {
        self.selectedIndexPath = indexPath//記錄封面索引位置
        ...
        self.navigationController?.pushViewController(toVC, animated: true)
    }
}
實現 Push

一切準備就緒,回到動畫控制器,補充剩下的部分:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    ...
    switch operation{
    case .Push:
        //隱藏被選中的封面,同時添加偽裝的封面到 toView 里
        let selectedCell = fromVC?.collectionView?.cellForItemAtIndexPath(fromVC!.selectedIndexPath)
        selectedCell?.hidden = true
        //計算偽裝的位置,這個位置對于后面添加偽裝的封面和調整 visibleCells 至關重要。
        let layoutAttributes = fromVC!.collectionView?.layoutAttributesForItemAtIndexPath(fromVC!.selectedIndexPath)
        let areaRect = fromVC!.collectionView?.convertRect(layoutAttributes!.frame, toView: fromVC!.collectionView?.superview)
        toVC!.coverRectInSuperview = areaRect!
        let fakeCoverView = createAndSetupFakeCoverView(fromVC!, toVC: toVC!)

        //強制刷新 toView,以便能夠在 toVC 的collectionView 被顯示之前能夠獲取 visibleCells。
        toVC?.view.layoutIfNeeded()
        //針對 visibleCells 調整大小和位置,以便能夠隱藏在封面后面,此處比較繁瑣,想知道具體實現的話可以看源碼
        setupVisibleCellsBeforePushToVC(toVC!)
        //添加 toView, toView 將會出現在屏幕上
        containerView?.addSubview(toView!)

        UIView.setAnimationCurve(UIViewAnimationCurve.EaseOut)
        let options: UIViewKeyframeAnimationOptions = [.BeginFromCurrentState, .OverrideInheritedDuration, .CalculationModeCubic, .CalculationModeLinear]
        //key frame animation 里添加的動畫的時間都是針對 duration 進行比例計算的,開始時間和持續時間的值都在0和1之間。
        UIView.animateKeyframesWithDuration(duration, delay: 0.0, options: options, animations: {
            //將上面實現的多步動畫添加到這里
            self.addkeyFrameAnimationForBackgroundColorInPush(fromVC!, toVC: toVC!)
            self.addKeyFrameAnimationInPushForFakeCoverView(self.fakeCoverView)
            self.addKeyFrameAnimationOnVisibleCellsInPushToVC(toVC!)
            }, completion: { finished in
                let isCancelled = transitionContext.transitionWasCancelled()
                //如果 push 被取消,則將一切恢復原樣,恢復原裝封面的顯示
                if isCancelled{
                    selectedCell?.hidden = false
                }
                transitionContext.completeTransition(!isCancelled)
        })
    ...
    }
}
實現 Pop

Pop 過程中的動畫基本上是對 push 過程的逆向,唯一需要注意的地方是由于用戶可能會滑動 collectionView,那么 pop 時的 visibleCells 可能和 push 時的不一樣,這時候要注意調整有關計算相對位置的算法,具體可以看代碼。這里有個問題,用戶在滑動還沒有結束時點擊返回,此時的 pop 動畫就露餡了,因為位置是相對于返回的那一刻在計算的,而界面依然在滑動,封面下面的照片會超出封面的范圍。

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    ...
    case .Push:
        ...
    case .Pop:
         //fromVC 和 fromView 都是指代當前顯示的視圖控制器和視圖,與操作類型是 push 還是 pop 無關。
        //需要注意的是,此時不能再簡單地使用addSubview:,不然 fromView 會被擋住不可見
        containerView?.insertSubview(toView!, belowSubview: fromView!)
        //根據 tag 來獲取偽裝的封面
        let coverView = fromView?.viewWithTag(1000)
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        UIView.animateKeyframesWithDuration(duration, delay: 1.0, options: UIViewKeyframeAnimationOptions(), animations: {
            //pop 過程的動畫基本上是對 push 過程中動畫的逆向。唯一需要注意的是,push 和 pop 時的 visibleCells 可能會不同,需要做出調整,具體看代碼
            self.addkeyFrameAnimationForBackgroundColorInPop(fromVC!)
            self.addKeyFrameAnimationInPopForFakeCoverView(coverView)
            self.addKeyFrameAnimationOnVisibleCellsInPopFromVC(fromVC!)
            }, completion: { finished in
                let isCancelled = transitionContext.transitionWasCancelled()
                //只有 pop 過程完成了,才能恢復源封面的顯示
                if !isCancelled{
                    let selectedCell = toVC?.collectionView?.cellForItemAtIndexPath(toVC!.selectedIndexPath)
                    selectedCell?.hidden = false
                }
                transitionContext.completeTransition(!isCancelled)
        })
}

這樣就完成了非交互動畫,接下來在這里討論下如何使用 pinch 手勢來控制 push 和 pop 過程。

說點什么

這么一口氣看下來,對剛開始接觸的人來說有點困難,對有過類似經驗的人來說,應該也能找到點新的東西。如果你還沒有試過將這個過程交互化,那么這篇內容已經規避了大部分交互動畫的陷阱,正如那些加粗顯示的內容提示的那樣,也正因為如此在下篇里才會顯得如此輕松。三個月前的 Demo 也做了和如今大部分都相同的東西,但現在的 Demo 有著更好的解耦性,更方便使用,這也是個進步。

參考資料:
1. WWDC13 Session 218: Custom Transitions Using View Controllers
2.《自定義 ViewController 容器轉場》
3.《Custom Transitions on iOS》,此文是我見過關于 ViewController Custom Transition 的最好文章,強烈推薦。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容