iOS 本身定義頁面的切換動畫與交互動畫 By Swift

在iOS7以前,開發人員爲了尋求本身定義Navigation Controller的Push/Pop動畫,僅僅能受限於子類化一個UINavigationController,或是用本身定義的動畫去覆蓋它。但是隨着iOS7的到來,Apple針對開發人員推出了新的工具,以更靈活地方式管理UIViewController切換。git


我把終於的Demo稍作改動,算是找了一個合適的應用場景,另外配上幾張美圖,拉拉人氣偷笑github

儘管是Swift的Demo,但是轉成Objective-C至關easy。ide


終於效果預覽:


本身定義導航欄的Push/Pop動畫

爲了在基於UINavigationController下作本身定義的動畫切換,先創建一個簡單的project,這個project的rootViewController是一個UINavigationController,UINavigationController的rootViewController是一個簡單的UIViewController(稱之爲主頁面),經過這個UIViewController上的一個Button能進入到下一個UIViewController中(稱之爲詳情頁面),咱們先在主頁面的ViewController上實現兩個協議:UINavigationControllerDelegate和UIViewControllerAnimatedTransitioning,而後在ViewDidLoad裏面把navigationController的delegate設爲self,這樣在導航欄Push和Pop的時候咱們就知道了,而後用一個屬性記下是Push仍是Pop,就像這樣:工具

func navigationController(navigationController: UINavigationController!, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController!, toViewController toVC: UIViewController!) -> UIViewControllerAnimatedTransitioning! {動畫

    navigationOperation = operationspa

    return self.net

}code

這是iOS7的新方法,這種方法需要你提供一個UIViewControllerAnimatedTransitioning,那UIViewControllerAnimatedTransitioning到底是什麼呢?orm

UIViewControllerAnimatedTransitioning是蘋果新添加的一個協議,其目的是在需要使用本身定義動畫的同一時候,又不影響視圖的其它屬性,讓你把焦點集中在動畫實現的自己上,而後經過在這個協議的回調裏編寫本身定義的動畫代碼,即「切換中應該會發生什麼」,負責切換的詳細內容,不論什麼實現了這一協議的對象被稱之爲動畫控制器。你可以藉助協議能被不論什麼對象實現的這一特性,從而把各類動畫效果封裝到不一樣的類中,僅僅要方便使用和管理,你可以發揮一切手段。我在這裏讓主頁面實現動畫控制器也是可以的,因爲它是導航欄的rootViewController,會一直存在,我僅僅要在裏面編寫本身定義的Push和Pop動畫代碼就可以了:對象

//UIViewControllerTransitioningDelegate

func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {

    return 0.4

}


func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {

    let containerView = transitionContext.containerView()

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    var destView: UIView!

    var destTransform: CGAffineTransform!

    if navigationOperation == UINavigationControllerOperation.Push {

        containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)

        destView = toViewController.view

        destView.transform = CGAffineTransformMakeScale(0.10.1)

        destTransform = CGAffineTransformMakeScale(11)

    } else if navigationOperation == UINavigationControllerOperation.Pop {

        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

        destView = fromViewController.view

        // 假設IDEXcode6 Beta4+iOS8SDK,那麼在此處設置爲0,動畫將不會被運行(不肯定是哪裏的Bug)

        destTransform = CGAffineTransformMakeScale(0.10.1)

    }

    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {

        destView.transform = destTransform

        }, completion: ({completed in

            transitionContext.completeTransition(true)

        }))

}

上面第一個方法返回動畫持續的時間,而如下這種方法纔是詳細需要實現動畫的地方。UIViewControllerAnimatedTransitioning的協議都包括一個對象:transitionContext,經過這個對象能獲取到切換時的上下文信息,比方從哪一個VC切換到哪一個VC等。咱們從transitionContext獲取containerView,這是一個特殊的容器,切換時的動畫將在這個容器中進行;UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey就是從哪一個VC切換到哪一個VC,easy理解;除此以外,還有直接獲取view的UITransitionContextFromViewKey和UITransitionContextToViewKey等。

我按Push和Pop把動畫簡單的區分了一下,Push時scale由小變大,Pop時scale由大變小,不一樣的操做,toViewController的視圖層次也不同。最後,在動畫完畢的時候調用completeTransition,告訴transitionContext你的動畫已經結束,這是很重要的方法,必須調用。在動畫結束時沒有對containerView的子視圖進行清理(比方把fromViewController的view移除掉)是因爲transitionContext會本身主動清理,因此咱們無須在額外處理。

注意一點,這樣一來會發現原來導航欄的交互式返回效果沒有了,假設你想用原來的交互式返回效果的話,在返回動畫控制器的delegate方法裏返回nil,如:

if operation == UINavigationControllerOperation.Push {

    navigationOperation = operation

    return self

}

return nil

而後在viewDidLoad裏,Objective-C直接self.navigationController.interactivePopGestureRecognizer.delegat = self就能夠了,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self以外,還要在self上聲明實現了UIGestureRecognizerDelegate這個協議,儘管實際上你並無實現。

一個簡單的本身定義導航欄Push/Pop動畫就完畢了。


本身定義Modal的Present/Dismiss動畫

本身定義Modal的Present與Dismiss動畫與以前類似,都需要提供一個動畫管理器,咱們用詳情頁面來展現一個Modal頁面,詳情頁面就做爲動畫管理器:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {

    return 0.6

}


func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {

    let containerView = transitionContext.containerView()

    

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    var destView: UIView!

    var destTransfrom = CGAffineTransformIdentity

    let screenHeight = UIScreen.mainScreen().bounds.size.height

    

    if modalPresentingType == ModalPresentingType.Present {

        destView = toViewController.view

        destView.transform = CGAffineTransformMakeTranslation(0, screenHeight)

        containerView.addSubview(toViewController.view)

    } else if modalPresentingType == ModalPresentingType.Dismiss {

        destView = fromViewController.view

        destTransfrom = CGAffineTransformMakeTranslation(0, screenHeight)

        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

    }

    

    UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0,

        options: UIViewAnimationOptions.CurveLinear, animations: {

            destView.transform = destTransfrom

        }, completion: {completed in

            transitionContext.completeTransition(true)

    })

}

動畫部分用了一個iOS7的彈簧動畫,usingSpringWithDamping的值設置得越小,彈的就越明顯,動畫的其它地方與以前類似,不同的是以前主頁面除了作動畫管理器以外,還實現了UINavigationControllerDelegate協議,因爲咱們是本身定義導航欄的動畫,而在這裏需要本身定義Modal動畫就要實現還有一個協議: UIViewControllerTransitioningDelegate,這個協議與以前的UINavigationControllerDelegate協議具備類似性,都是返回一個動畫管理器,iOS7的方法總共同擁有四個,有兩個交互式的先不管,咱們僅僅需要實現另兩個就能夠:

func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {

    modalPresentingType = ModalPresentingType.Present

    return self

}


func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {

    modalPresentingType = ModalPresentingType.Dismiss

    return self

}

我相同的用一個屬性記下是Present仍是Dismiss,而後返回self。因爲我是用的Storyboard,因此需要在prepareForSegue方法裏設置一下transitionDelegate:

override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {

    let modal = segue.destinationViewController as UIViewController

    modal.transitioningDelegate = self

}

對需要運行本身定義動畫的VC設置transitionDelegate屬性就能夠。
如此一來,一個針對模態VC的本身定義動畫也完畢了。

本身定義導航欄的交互式動畫

與動畫控制器類似,咱們把實現了 UIViewControllerInteractiveTransitioning協議的對象稱之爲 交互控制器,最常用的就是把交互控制器應用到導航欄的Back手勢返回上,而假設要實現一個本身定義的交互式動畫,咱們有兩種方式來完畢:實現一個交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition類做交互控制器。

使用UIPercentDrivenInteractiveTransition

咱們這裏就用UIPercentDrivenInteractiveTransition來完畢導航欄的交互式動畫。先看下UIPercentDrivenInteractiveTransition的定義:

實際上這個類就是實現了UIViewControllerInteractiveTransitioning協議的交互控制器,咱們使用它就可以輕鬆地爲動畫控制器加入一個交互動畫。調用updateInteractiveTransition:更新進度;調用cancelInteractiveTransition取消交互,返回到切換前的狀態;調用finishInteractiveTransition通知上下文交互已完畢,同completeTransition同樣。咱們把交互動畫應用到詳情頁面Back回主頁面的地方,因爲以前的動畫管理器的角色是主頁面擔任的,Navigation Controller的delegate同一時間僅僅能有一個,那在這裏交互控制器的角色也由主頁面來擔任。首先加入一個手勢識別器:

let popRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: Selector("handlePopRecognizer:"))

popRecognizer.edges = UIRectEdge.Left

self.navigationController.view.addGestureRecognizer(popRecognizer)

UIScreenEdgePanGestureRecognizer繼承於UIPanGestureRecognizer,能檢測從屏幕邊緣滑動的手勢,設置edges爲left檢測左邊就能夠。而後實現handlePopRecognizer:

func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {

    var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width

    progress = min(1.0max(0.0, progress))

    

    println("\(progress)")

    if popRecognizer.state == UIGestureRecognizerState.Began {

        println("Began")

        self.interactivePopTransition = UIPercentDrivenInteractiveTransition()

        self.navigationController.popViewControllerAnimated(true)

    } else if popRecognizer.state == UIGestureRecognizerState.Changed {

        self.interactivePopTransition?.updateInteractiveTransition(progress)

        println("Changed")

    } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {

        if progress > 0.5 {

            self.interactivePopTransition?.finishInteractiveTransition()

        } else {

            self.interactivePopTransition?.cancelInteractiveTransition()

        }

        println("Ended || Cancelled")

        self.interactivePopTransition = nil

    }

}

我用了一個實例變量引用UIPercentDrivenInteractiveTransition,這個類僅僅在需要用時才建立,不然在正常Push/Pop的時候,即便僅僅是點擊操做並無識別手勢的狀況下,也會進入交互(你也可以在要求你返回交互控制器時,進行一些推斷,經過返回nil來屏蔽,但這顯然就太麻煩了)。當手勢識別的時候咱們調用pop,用戶手勢發生變化時,調用update去更新,不管是end仍是cancel,都推斷下是進入下一個頁面仍是返回以前的頁面,完畢這一切後把交互控制器清理掉。

現在咱們已經有了交互控制器對象,僅僅需要把它給告知給Navigation Controller就能夠了,咱們實現UINavigationControllerDelegate的還有一個方法:

func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {

    return self.interactivePopTransition

}

咱們從詳情頁面經過本身定義的交互動畫返回到上一個頁面的工做就完畢了。

Demo效果預覽:


使用UIPercentDrivenInteractiveTransition的Demo


本身定義交互控制器

我在以前提過,UIPercentDrivenInteractiveTransition實際上就是實現了 UIViewControllerInteractiveTransitioning協議,僅僅要是實現了這個協議的對象就可以稱之爲交互控制器,咱們假設想更加精確的管理動畫以及深刻理解處理上的細節,就需要本身實現 UIViewControllerInteractiveTransitioning協議。
UIViewControllerInteractiveTransitioning協議總共同擁有三個方法,當中startInteractiveTransition:是必須實現的方法,咱們在裏面初始化動畫的狀態:

func startInteractiveTransition(transitionContext: UIViewControllerContextTransitioning!) {

    self.transitionContext = transitionContext

    

    let containerView = transitionContext.containerView()

    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)

    

    containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

    

    self.transitingView = fromViewController.view

}

這裏不涉及動畫,僅僅是把需要切換的view加入到上下文環境中就能夠。動畫部分咱們仍是和以前使用UIPercentDrivenInteractiveTransition的接口保持一致,加入幾個方法:

func updateWithPercent(percent: CGFloat) {

    let scale = CGFloat(fabsf(Float(percent - CGFloat(1.0))))

    transitingView?.transform = CGAffineTransformMakeScale(scale, scale)

    transitionContext?.updateInteractiveTransition(percent)

}


func finishBy(cancelled: Bool) {

    if cancelled {

        UIView.animateWithDuration(0.4, animations: {

            self.transitingView!.transform = CGAffineTransformIdentity

            }, completion: {completed in

                self.transitionContext!.cancelInteractiveTransition()

                self.transitionContext!.completeTransition(false)

        })

    } else {

        UIView.animateWithDuration(0.4, animations: {

            print(self.transitingView)

            self.transitingView!.transform = CGAffineTransformMakeScale(00)

            print(self.transitingView)

            }, completion: {completed in

                self.transitionContext!.finishInteractiveTransition()

                self.transitionContext!.completeTransition(true)

        })

    }

}

updateWithPercent:方法用來更新view的transform屬性,finishBy:方法主要用來推斷是進入下一個頁面仍是返回到以前的頁面,並告知transitionContext眼下的狀態,以及對當前正在scale的view作最後的動畫。這裏的transitionContext和transitingView可以在前面的處理手勢識別代碼中取得,我將裏面的代碼更新了一下,變成如下這樣:

func handlePopRecognizer(popRecognizer: UIScreenEdgePanGestureRecognizer) {

    var progress = popRecognizer.translationInView(navigationController.view).x / navigationController.view.bounds.size.width

    progress = min(1.0max(0.0, progress))

    

    println("\(progress)")

    if popRecognizer.state == UIGestureRecognizerState.Began {

        println("Began")

        isTransiting = true

        //self.interactivePopTransition = UIPercentDrivenInteractiveTransition()

        self.navigationController.popViewControllerAnimated(true)

    } else if popRecognizer.state == UIGestureRecognizerState.Changed {

        //self.interactivePopTransition?.updateInteractiveTransition(progress)

        updateWithPercent(progress)

        println("Changed")

    } else if popRecognizer.state == UIGestureRecognizerState.Ended || popRecognizer.state == UIGestureRecognizerState.Cancelled {

        //if progress > 0.5 {

        //    self.interactivePopTransition?.finishInteractiveTransition()

        //} else {

        //    self.interactivePopTransition?.cancelInteractiveTransition()

        //}

        finishBy(progress < 0.5)

        println("Ended || Cancelled")

        isTransiting = false

        //self.interactivePopTransition = nil

    }

}

另外還用一個額外布爾值變量isTransiting來標識當前是否在手勢識別中,這是爲了在返回交互控制器的時候,不會在不當的時候返回self:

func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController:

UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {

    if !self.isTransiting {

        return nil

    }

    return self

}

這樣一來就完畢了本身定義交互控制器。可以發現,基本流程與使用UIPercentDrivenInteractiveTransition是一致的,UIPercentDrivenInteractiveTransition主要是幫咱們封裝了transitionContext的初始化以及對它的調用等,僅僅是動畫部分需要咱們在額外處理一下了。

終於效果:

我在主頁面上多放了幾個帶Image的Button,在點擊Button時會將Button的Image傳遞到詳情頁面,詳情頁面相應的也有一個UIImageView用來顯示。在主頁面初始化動畫狀態的時候,會生成一個Image的快照來進行動畫,要是在曾經,咱們僅僅能經過UIGraphics的APIs進行一系列的操做,涉及視圖的scale、旋轉、透明及渲染到context等,但現在,咱們僅僅需要用iOS7的API就能夠了:

@availability(iOS, introduced=7.0)

func snapshotViewAfterScreenUpdates(afterUpdates: Bool) -> UIView

這個API能幫助咱們高速獲取一個視圖的的快照,afterUpdates參數表示是否等所有效果應用到該視圖以後再獲取,假設設置爲false,則立刻獲取;爲true則會受到後面對該視圖的影響。
在動畫以前,把主頁面和詳情頁面相應的Button和ImageView隱藏,而後對快照生成的View進行動畫,動畫用簡單的frame隱式動畫就可以了。

終於效果的Demo (上傳到個人資源頁面時老是失敗,因此僅僅能上傳到GitHub上了)

最後附上一張圖,這個圖比較easy區分那幾個名稱相近的協議:



UPDATED:

GitHub上已更新至Xcode 6,主要是語法上的一些小調整
相關文章
相關標籤/搜索