Swift:超炫的View Controller切換動畫

匿名社交應用Secret的開發者開發了一款叫作Ping的應用,用戶能夠他們感興趣的話題的推送。app

Ping有一個很炫的東西,就是主界面和之間切換的動畫作的很是的好。每次看到一個很是炫的動畫,都不禁得會想:「這個東西我要不要本身實現如下」。哈哈~~~ide

這個教程裏,你會學到如何用Swift實現這樣的很酷的動畫。你會學到如何使用shape layer,遮罩和使用UIViewControllerAnimnatedTransitioning協議和UIPercentDrivenInteractivetransition類等實現View Controller界面切換動畫。動畫

不過須要注意,這裏假定你已經有必定的Swift開發基礎。若是隻是初學的話,請自行查看我得其餘Swift教程。spa

 

開篇簡介代理

咱們主要介紹Ping裏從一個View Controller跳轉到另外一個的時候的動畫。code

在iOS裏,你能夠在UINavigationController中放入兩個View Controller,並實現UIViewControllerAnimatedTransitioning協議來實現界面切換的動畫。具體的細節有:orm

  • 動畫的時間長度
  • 建立一個容器View來控制兩個View Controller的View
  • 能夠實現任意你能想到的動畫

這些動畫,你能夠用UIView得動畫方法來做,也能夠用core animation這樣的比較底層的方法來作。本教程會使用後者。對象

 

如何實現blog

如今你已經知道代碼大概會添加到什麼地方。下面討論下如何實現那個Ping的那個圈圈動畫。這動畫嚴格的描述起來是:教程

  • 圓圈是從右側的按鈕產生。而且從圈中能夠看到下面一層試圖的內容。
  • 也就是說,這個圓圈是一個遮罩。圓圈裏的均可以看到,外面的所有都隱藏。

你能夠用CALayer的mask能夠達到這個效果。固然還須要設置alpha爲0來隱藏下面一個視圖的內容。alpha值設定爲1的時候顯示下面視圖的內容。

 

如今你就懂了遮罩了。下一步就是決定用哪種CAShapeLayer來實現這個遮罩。只須要修改這些CAShapeLayer組成的圓圈的半徑。

 

如今開始

這裏就不十分詳細的敘述了,都是些關於建立和配置項目的步驟。

1. 建立一個新的項目。選擇一個single view application

2. 項目名稱設置爲CircleTransition。語言選擇Swift。Devices就選擇iPhone

項目到此初步建立好了。在Main.stroyboard裏只有一個view controller。可是咱們的動畫須要兩個至少的view controller。不過首先須要把如今的這個view controller和UINavigationController關聯起來。選中這個惟一的view controller,以後在菜單欄中選擇Editor->Embed In->Navigation Controller。以後這個navigation controller就會成爲initial controller,後面連着最開始生成的那個view controller。以後,選中這個navigation controller,在右側菜單欄的第四個tab中勾去「Shows navigation bar」。由於在咱們的app中不須要navigation bar。

 接下來添加另一個view controller。給這個view controller指定class爲ViewController。

而後,給每個view controller,除了navigation controller,添加一個按鈕。雙擊按鈕,刪除文字,以後把按鈕的背景色設置爲黑色。另一個按鈕也一樣處理。給這兩個按鈕設定autolayout。指定他們在右上角上。指定這兩個按鈕的寬度和高度爲40。

最後讓按鈕變成圓形的。右邊菜單的第三個tab中選擇「user defined runtime attributes」。點下面的加號,添加如圖所示的內容。設置button的corner radius爲15。

 這樣這個按鈕在運行起來的時候就是圓形的了。設定完成以後暫時看不到這個效果。運行起來之後:

如今須要在每一個view controller中添加些內容了。先把這兩個view controller的背景色修改一下。

如今這個app大體已經成型了。不一樣的顏色能夠表明你未來要顯示出來的各類各樣的內容。所須要的就是把這個兩個view controller連起來。在橘色的controller的按鈕中放下鼠標。按下ctrl而後把光標拖動到另一個controller上。這是會出現一個彈出的菜單。把這個菜單的action用一樣的方法和這個controller再鏈接一次,並選擇show。這樣,在這個按鈕選擇的時候,navigation controller就會push到下一個view controller中。這是一個segue。後面的教程會須要這個segue因此這裏給這個segue一個identifer,叫作「PushSegue」。運行代碼,點擊橘色controller的按鈕就會跳轉到紫色的controller了。

由於這是一個循環的過程,因此從橘色到紫色以後還須要從紫色回到橘色。如今就完成這個功能。首先,在紫色controller綁定的ViewController類中添加一個action方法。

    @IBAction func circleTapped(sender: UIButton){
        self.navigationController?.popViewControllerAnimated(true)
    }

並添加紫色controller上的按鈕的引用,這個會在後面用到:

@IBOutlet weak var button: UIButton!

以後給紫色controller的按鈕的「touch up inside」事件添加上面的@IBAction。

綁定按鈕的屬性:

再運行起來看看。橘色到紫色,紫色到橘色循環往復!

注意:兩個view controller都須要綁定按鈕和按鈕事件!不然後面的動畫只能執行一次!

 

自定義動畫

這裏主要處理的就是navigation controller的push和pop動畫。這須要咱們實現UINavigationControllerDelegate協議的animationControllerForOperation方法。直接在ViewController中添加一個新的類:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
}

首先,在右側的菜單中選中Object這個item。

以後,把這個東西拖動到navigation controller secene下。

而後選中這個Object,在右側菜單的第三個tab上修改class爲咱們剛剛定義的NavigationControllerDelegate

下一步,給navigation controller指定delegate。選中navigation controller,而後在右側最後的菜單中鏈接navigation controller的delegate選項到剛剛拖進來的Object上:

這個時候仍是不會有特定的效果出現。由於方法仍是空的,只能算是一個placeholder方法。

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
}

這個方法接受兩個在navigation controller中得controller。從一個跳轉到另外一個的兩個controller。並返回一個實現了UIViewControllerAnimatedTransitioning的對象。因此,咱們須要建立一個實現了UIViewControllerAnimatedTransitioning協議的類。

class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning

首先添加一個屬性:

weak var transitionContext: UIViewControllerContextTransitioning?

這個屬性會在後面的代碼中用到。

添加一個方法:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 0.5
}

這個方法返回動畫執行的時間。

添加動畫方法:

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // 1
        self.transitionContext = transitionContext
        
        // 2
        var containerView = transitionContext.containerView()
        var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as ViewController
        var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as ViewController
        var button = fromViewController.button
        
        // 3
        containerView.addSubview(toViewController.view)
        
        // 4
        var circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame)
        var extremePoint = CGPointMake(button.center.x, button.center.y - CGRectGetHeight(toViewController.view.bounds)) // need more research
        var radius = sqrt(extremePoint.x * extremePoint.x + extremePoint.y * extremePoint.y)
        var circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius))
        
        // 5
        var maskLayer = CAShapeLayer()
        maskLayer.path = circleMaskPathFinal.CGPath
        toViewController.view.layer.mask = maskLayer
        
        // 6
        var maskLayerAnimation = CABasicAnimation(keyPath: "path")
        maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
        maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
        maskLayerAnimation.duration = self.transitionDuration(self.transitionContext!)
        maskLayerAnimation.delegate = self
        maskLayer.addAnimation(maskLayerAnimation, forKey: "CircleAnimation")
    }

一步步的解釋:

  1. transitionContext屬性保持了一個類成員的引用。這樣在後面的代碼中能夠用到。
  2. 取出containerView以及fromViewController和toViewController和controller上面的button引用。動畫主要仍是做用在container view上的。
  3. 把toViewController的view添加到container view上。
  4. 建立兩個路勁,一個就是button的大小(button在運行起來以後是圓形的),另外一個要足夠大到能夠cover整個screen。動畫就是在這兩個path上來來回回。
  5. 建立一個CAShapeLayer做爲mask用。給這個layer的path賦值爲circleMaskPathFinal,不然動畫執行完成之後可能又縮回來。
  6. 建立一個CABasicAnimation動畫,key path是「path」,這個動畫做用於layer的path屬性上。動畫從circleMaskPathInitial執行到circleMaskPathFinal。並給這個動畫添加一個delegate,在動畫執行完成之後清理現場。

實現animation代理的方法:

    override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
        self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
        self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
    }

如今就能夠用CircleTransitionAnimator來實現動畫的效果了。修改代碼NavigationControllerDelegate的代碼:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CircleTransitionAnimator()
    }
}

運行起來吧。點擊黑色的按鈕,動畫效果就出現了。

感受不錯吧,可是這個是不夠的!

 

給動畫添加手勢響應

咱們還要給這個動畫添加一個能夠響應手勢的transition。響應手勢須要用到一個方法:navigationController->interactionControllerForAnimationController。這是UINavigationControllerDelegate中得一個方法。這個方法返回一個實現了協議UIViewControllerInteractiveTransitioning的對象。

iOS的SDK中提供了一個UIPercentDrivenInteractiveTransition的類。這個類實現了上面的協議,而且提供了不少其餘的手勢處理實現。

NavigationControllerDelegate類中添加如下的屬性和方法:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate{
    
    var interactionController: UIPercentDrivenInteractiveTransition?
    
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CircleTransitionAnimator()
    }
    
    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return self.interactionController }
}

既然是響應手勢的,那麼一個pan的手勢是必不可少的了。不過首先要添加一些輔助的東西。

1. 在NavigationControllerDelegate中添加對navigation controller的引用。

@IBOutlet weak var navigationController: UINavigationController?

給這個引用添加對navigation controller的引用,如圖:

實現awakeFromNib方法:

    override func awakeFromNib() {
        super.awakeFromNib()
        
        var pan = UIPanGestureRecognizer(target: self, action: "panned:")
        self.navigationController!.view.addGestureRecognizer(pan)
    }

當pan這個動做在navigation controller的view上發生的時候就會觸發panned回調方法。給這個方法添加以下代碼:

    func panned(gestureRecognizer: UIPanGestureRecognizer){
        switch gestureRecognizer.state {
        case .Began:
            self.interactionController = UIPercentDrivenInteractiveTransition()
            if self.navigationController?.viewControllers.count > 1 {
                self.navigationController?.popViewControllerAnimated(true)
            }
            else{
                self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil)
            }
        case .Changed:
            var translation = gestureRecognizer.translationInView(self.navigationController!.view)
            var completionProgress = translation.x / CGRectGetWidth(self.navigationController!.view.bounds)
            self.interactionController?.updateInteractiveTransition(completionProgress)
        case .Ended:
            if gestureRecognizer.velocityInView(self.navigationController!.view).x > 0 {
                self.interactionController?.finishInteractiveTransition()
            }
            else{
                self.interactionController?.cancelInteractiveTransition()
            }
            self.interactionController = nil
        default:
            self.interactionController?.cancelInteractiveTransition()
            self.interactionController = nil
        }
    }

在Begin中,pan手勢一開始執行就初始化出UIPercentDrivenInteractiveTransition對象,並做爲值賦給屬性self.interactionController。

  • 若是在第一個view controller就設定一個push(在早先定義的一個segue),在第二個view controller的時候就設定一個pop。
  • 在navigation controller的push或者pop的時候則觸發NavigationControllerDelegate的返回self.interactionController對象的方法。

Changed,在這個方法中根據手勢移動的距離讓動畫移動不一樣的距離。這裏apple已經替咱們作了不少。

Ended,這裏你會看到手勢的移動速度。若是是正則transition結束,若是是負則取消。同時,把interactionController值設置爲nil。

default,若是是其餘的狀態就直接取消trnasition並把interactionController值設置爲nil。

 

運行程序,在屏幕上左右移動你的手指看看效果吧!

相關文章
相關標籤/搜索