ios7中 豐富多彩的UIViewController之間的切換

來源:互聯網
上載者:User

 

 

毫無疑問,ViewController(在本文中簡寫為VC)是使用MVC構建Cocoa或者CocoaTouch程式時最重要的一個類,我們的日常工作中一般來說最花費時間和精力的也是在為VC部分編寫代碼。蘋果產品是注重使用者體驗的,而對細節進行琢磨也是蘋果對於開發人員一直以來的要求和希望。在使用者體驗中,VC之間的關係,比如不同VC之間遷移和轉換動畫效果一直是一個值得不斷推敲的重點。在iOS7中,蘋果給出了一套完整的VC製作之間遷移效果的方案,可以說是為現在這部分各種不同實現方案指出了一條推薦的統一道路。

iOS 7 SDK之前的VC切換解決方案

在深入iOS 7的VC轉場效果的新API實現之前,先讓我們回顧下現在的一般做法吧。這可以協助理解為什麼iOS7要對VC切換給出新的解決方案,如果您對iOS 5中引入的VC容器比較熟悉的話,可以跳過這節。

在iOS5和iOS6中,除了標準的Push,Tab和PresentModal之外,一般是使用ChildViewController的方式來完成VC之間切換的過渡效果。ChildViewController和自訂的Controller容器是iOS 5 SDK中加入的,可以用來產生自訂的VC容器,簡單來說典型的一種用法類似這樣:

//ContainerVC.m[self addChildViewController:toVC];[fromVC willMoveToParentViewController:nil];[self.view addSubview:toVC.view];__weak id weakSelf = self;  [self transitionFromViewController:fromVC                  toViewController:toVC duration:0.3                           options:UIViewAnimationOptionTransitionCrossDissolve                        animations:^{}                        completion:^(BOOL finished) {    [fromVC.view removeFromSuperView];    [fromVC removeFromParentViewController];    [toVC didMoveToParentViewController:weakSelf];}];

在自己對view進行管理的同時,可以使用transitionFromViewController:toViewController:...的Animation block中可以實現一些簡單的轉場效果。去年年初我寫的UIViewController的誤用一文中曾經指出類似[viewController.view addSubview:someOtherViewController.view];這樣的代碼的存在,一般就是誤用VC。這個結論適用於非Controller容器,對於自訂的Controller容器來說,向當前view上添加其他VC的view是正確的做法(當然不能忘了也將VC本身通過addChildViewController:方法添加到容器中)。

VC容器的主要目的是解決將不同VC添加到同一個螢幕上的需求,以及可以提供一些簡單的自訂切換開關效果。使用VC容器可以使view的關係正確,使添加的VC能夠正確接收到例如旋轉螢幕,viewDidLoad:等VC事件,進而進行正確相應。VC容器確實可以解決一部分問題,但是也應該看到,對於自訂切換開關效果來說,這樣的解決還有很多不足。首先是代碼高度耦合,VC切換部分的代碼直接寫在container中,難以分離重用;其次能夠提供的轉場效果比較有限,只能使用UIView動畫來切換,管理起來也略顯麻煩。iOS 7提供了一套新的自訂VC切換,就是針對這兩個問題的。

iOS 7 自訂ViewController動畫切換自訂動畫切換的相關的主要API

在深入之前,我們先來看看新SDK中有關這部分內容的相關介面以及它們的關係和典型用法。這幾個介面和類的名字都比較相似,但是還是能比較好的描述出各自的職能的,一開始的話可能比較迷惑,但是當自己動手實現一兩個例子之後,它們之間的關係就會逐漸明晰起來。(相關的內容都定義在UIKit的UIViewControllerTransitioning.h中了)

@protocol UIViewControllerContextTransitioning

這個介面用來提供切換上下文給開發人員使用,包含了從哪個VC到哪個VC等各類資訊,一般不需要開發人員自己實現。具體來說,iOS7的自訂切換開關目的之一就是切換相關代碼解耦,在進行VC切換時,做轉場效果實現的時候必須要需要切換前後VC的一些資訊,系統在新加入的API的比較的地方都會提供一個實現了該介面的對象,以供我們使用。

對於切換的動畫實現來說(這裡先介紹簡單的動畫,在後面我會再引入手勢驅動的動畫),這個介面中最重要的方法有:

  • -(UIView *)containerView; VC切換所發生的view容器,開發人員應該將切出的view移除,將切入的view加入到該view容器中。
  • -(UIViewController *)viewControllerForKey:(NSString *)key; 提供一個key,返回對應的VC。現在的SDK中key的選擇只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey兩種,分別表示將要切出和切入的VC。
  • -(CGRect)initialFrameForViewController:(UIViewController *)vc; 某個VC的初始位置,可以用來做動畫的計算。
  • -(CGRect)finalFrameForViewController:(UIViewController *)vc; 與上面的方法對應,得到切換結束時某個VC應在的frame。
  • -(void)completeTransition:(BOOL)didComplete; 向這個context報告切換已經完成。@protocol UIViewControllerAnimatedTransitioning

    這個介面負責切換的具體內容,也即“切換中應該發生什麼”。開發人員在做自訂切換開關效果時大部分代碼會是用來實現這個介面。它只有兩個方法需要我們實現:

    • -(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext; 系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,SDK會用這個時間來在百分比驅動的切換中進行幀的計算,後面再詳細展開)。

    • -(void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext; 在進行切換的時候將調用該方法,我們對於切換時的UIView的設定和動畫都在這個方法中完成。

      @protocol UIViewControllerTransitioningDelegate

      這個介面的作用比較簡單單一,在需要VC切換的時候系統會像實現了這個介面的對象詢問是否需要使用自訂的轉場效果。這個介面共有四個類似的方法:

      • -(id< UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

      • -(id< UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController *)dismissed;

      • -(id< UIViewControllerInteractiveTransitioning >)interactionControllerForPresentation:(id < UIViewControllerAnimatedTransitioning >)animator;

      • -(id< UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id < UIViewControllerAnimatedTransitioning >)animator;

        前兩個方法是針對動畫切換的,我們需要分別在呈現VC和解散VC時,給出一個實現了UIViewControllerAnimatedTransitioning介面的對象(其中包含切換時間長度和如何切換)。後兩個方法涉及互動式切換,之後再說。

        Demo

        還是那句話,一百行的講解不如一個簡單的小Demo,於是..it's demo time~ 整個demo的代碼我放到了github的這個頁面上,有需要的朋友可以參照著看這篇文章。

        我們打算做一個簡單的自訂的modalViewController的轉場效果。普通的present modal VC的效果大家都已經很熟悉了,這次我們先實現一個自訂的類似的modal present的效果,與普通效果不同的是,我們希望modalVC出現的時候不要那麼乏味的就簡單從底部出現,而是帶有一個彈性效果(這裡雖然是彈性,但是僅指使用UIView的類比動畫,而不設計iOS 7的另一個重要特性UIKit Dynamics。用UIKit Dynamics當然也許可以實現更逼真華麗的效果,但是已經超出本文的主題範疇了,因此不在這裡展開了。關於UIKit Dynamics,可以參看我之前關於這個主題的一篇介紹)。我們首先實現簡單的ModalVC彈出吧..這段非常基礎,就交待了一下背景,非初級人士請跳過程式碼片段..

        先定義一個ModalVC,以及相應的protocal和delegate方法:

        //ModalViewController.h@class ModalViewController;@protocol ModalViewControllerDelegate -(void) modalViewControllerDidClickedDismissButton:(ModalViewController *)viewController;@end@interface ModalViewController : UIViewController@property (nonatomic, weak) id delegate;@end//ModalViewController.m- (void)viewDidLoad{    [super viewDidLoad];    // Do any additional setup after loading the view.    self.view.backgroundColor = [UIColor lightGrayColor];    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);    [button setTitle:@Dismiss me forState:UIControlStateNormal];    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];    [self.view addSubview:button];}-(void) buttonClicked:(id)sender{    if (self.delegate && [self.delegate respondsToSelector:@selector(modalViewControllerDidClickedDismissButton:)]) {        [self.delegate modalViewControllerDidClickedDismissButton:self];    }}

        這個是很標準的modalViewController的實現方式了。需要多嘴一句的是,在實際使用中有的同學喜歡在-buttonClicked:中直接給self發送dismissViewController的相關方法。在現在的SDK中,如果當前的VC是被顯示的話,這個訊息會被直接轉寄到顯示它的VC去。但是這並不是一個好的實現,違反了程式設計的哲學,也很容易掉到坑裡,具體案例可以參看這篇文章的評論。

        所以我們用標準的方式來呈現和解散這個VC:

        //MainViewController.m- (void)viewDidLoad{    [super viewDidLoad];    // Do any additional setup after loading the view.    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);    [button setTitle:@Click me forState:UIControlStateNormal];    [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];    [self.view addSubview:button];}-(void) buttonClicked:(id)sender{    ModalViewController *mvc =  [[ModalViewController alloc] init];    mvc.delegate = self;    [self presentViewController:mvc animated:YES completion:nil];}-(void)modalViewControllerDidClickedDismissButton:(ModalViewController *)viewController{    [self dismissViewControllerAnimated:YES completion:nil];}

        測試一下,沒問題,然後我們可以開始實現自訂的轉場效果了。首先我們需要一個實現了UIViewControllerAnimatedTransitioning的對象..嗯,建立一個類來實現吧,比如BouncePresentAnimation:

        //BouncePresentAnimation.h@interface BouncePresentAnimation : NSObject@end//BouncePresentAnimation.m- (NSTimeInterval)transitionDuration:(id )transitionContext{    return 0.8f;}- (void)animateTransition:(id )transitionContext{    // 1. Get controllers from transition context    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];    // 2. Set init frame for toVC    CGRect screenBounds = [[UIScreen mainScreen] bounds];    CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];    toVC.view.frame = CGRectOffset(finalFrame, 0, screenBounds.size.height);    // 3. Add toVC's view to containerView    UIView *containerView = [transitionContext containerView];    [containerView addSubview:toVC.view];    // 4. Do animate now    NSTimeInterval duration = [self transitionDuration:transitionContext];    [UIView animateWithDuration:duration                          delay:0.0         usingSpringWithDamping:0.6          initialSpringVelocity:0.0                        options:UIViewAnimationOptionCurveLinear                     animations:^{                         toVC.view.frame = finalFrame;                     } completion:^(BOOL finished) {                         // 5. Tell context that we completed.                         [transitionContext completeTransition:YES];                     }];}

        解釋一下這個實現:

        1. 我們首先需要得到參與切換的兩個ViewController的資訊,使用context的方法拿到它們的參照;
        2. 對於要呈現的VC,我們希望它從螢幕下方出現,因此將初始位置設定到螢幕下邊緣;
        3. 將view添加到containerView中;
        4. 開始動畫。這裡的動畫時間長度和切換時間長度一致,都為0.8s。usingSpringWithDamping的UIView動畫API是iOS7新加入的,描述了一個類比彈簧動作的動畫曲線,我們在這裡只做使用,更多資訊可以參看相關文檔;(順便多說一句,iOS7中對UIView動畫添加了一個很方便的Category,UIViewKeyframeAnimations。使用其中方法可以為UIView動畫添加主要畫面格動畫)
        5. 在動畫結束後我們必須向context報告VC切換完成,是否成功(在這裡的動畫切換中,沒有失敗的可能性,因此直接pass一個YES過去)。系統在接收到這個訊息後,將對VC狀態進行維護。

          接下來我們實現一個UIViewControllerTransitioningDelegate,應該就能讓它工作了。簡單來說,一個比較好的地方是直接在MainViewController中實現這個介面。在MainVC中聲明實現這個介面,然後加入或變更為如下代碼:

          @interface MainViewController ()@property (nonatomic, strong) BouncePresentAnimation *presentAnimation;@end@implementation MainViewController- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];    if (self) {        // Custom initialization        _presentAnimation = [BouncePresentAnimation new];    }    return self;}-(void) buttonClicked:(id)sender{    //...    mvc.transitioningDelegate = self;    //...}- (id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{    return self.presentAnimation;}

          Believe or not, we have done. 跑一下,應該可以得到如下效果:

          手勢驅動的百分比切換

          iOS7引入了一種手勢驅動的VC切換的方式(互動式切換)。如果你使用系統的各種應用,在navViewController裡push了一個新的VC的話,返回時並不需要點擊左上的Back按鈕,而是通過從螢幕左側劃向右側即可完成返回操作。而在這個操作過程中,我們甚至可以撤銷我們的手勢,以取消這次VC轉移。在新版的Safari中,我們甚至可以用相同的手勢來完成網頁的後退功能(所以很大程度上來說螢幕底部的工具列成為了擺設)。如果您還不知道或者沒太留意過這個改動,不妨現在就拿手邊的iOS7這輩試試看,手機瀏覽的朋友記得切回來哦 :)

          我們這就動手在自己的VC切換中實現這個功能吧,首先我們需要在剛才的知識基礎上補充一些東西:

          首先是UIViewControllerContextTransitioning,剛才提到這個是系統提供的VC切換上下文,如果您深入看了它的標頭檔描述的話,應該會發現其中有三個關於InteractiveTransition的方法,正是用來處理互動式切換的。但是在初級的實際使用中我們其實可以不太理會它們,而是使用iOS 7 SDK已經給我們準備好的一個現成轉為互動式切換而新加的類:UIPercentDrivenInteractiveTransition。

          UIPercentDrivenInteractiveTransition是什麼

          這是一個實現了UIViewControllerInteractiveTransitioning介面的類,為我們預先實現和提供了一系列便利的方法,可以用一個百分比來控制互動式切換的過程。一般來說我們更多地會使用某些手勢來完成互動轉移(當然用的進階的話用其他的輸入..比如聲音,iBeacon距離或者甚至面部微笑來做輸入驅動也無不可,畢竟想象無極限嘛..),這樣使用這個類(一般是其子類)的話就會非常方便。我們在手勢識別中只需要告訴這個類的執行個體當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,十分方便。具體的幾個重要方法:

          • -(void)updateInteractiveTransition:(CGFloat)percentComplete 更新百分比,一般通過手勢識別的長度之類的來計算一個值,然後進行更新。之後的例子裡會看到詳細的用法
          • -(void)cancelInteractiveTransition 報告互動取消,返回切換前的狀態
          • –(void)finishInteractiveTransition 報告互動完成,更新到切換後的狀態@protocol UIViewControllerInteractiveTransitioning

            就如上面提到的,UIPercentDrivenInteractiveTransition只是實現了這個介面的一個類。為了實現互動式切換的功能,我們需要實現這個介面。因為大部分時候我們其實不需要自己來實現這個介面,因此在這篇入門中就不展開說明了,有興趣的童鞋可以自行鑽研。

            還有就是上面提到過的UIViewControllerTransitioningDelegate中的返回Interactive實現對象的方法,我們同樣會在互動式切換中用到它們。

            繼續Demo

            Demo time again。在剛才demo的基礎上,這次我們用一個向上划動的手勢來吧之前呈現的ModalViewController給dismiss掉~當然是互動切換,可以半途取消的那種。

            首先建立一個類,繼承自UIPercentDrivenInteractiveTransition,這樣我們可以省不少事兒。

            //SwipeUpInteractiveTransition.h@interface SwipeUpInteractiveTransition : UIPercentDrivenInteractiveTransition@property (nonatomic, assign) BOOL interacting;- (void)wireToViewController:(UIViewController*)viewController;@end//SwipeUpInteractiveTransition.m@interface SwipeUpInteractiveTransition()@property (nonatomic, assign) BOOL shouldComplete;@property (nonatomic, strong) UIViewController *presentingVC;@end@implementation SwipeUpInteractiveTransition-(void)wireToViewController:(UIViewController *)viewController{    self.presentingVC = viewController;    [self prepareGestureRecognizerInView:viewController.view];}- (void)prepareGestureRecognizerInView:(UIView*)view {    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];    [view addGestureRecognizer:gesture];}-(CGFloat)completionSpeed{    return 1 - self.percentComplete;}- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];    switch (gestureRecognizer.state) {        case UIGestureRecognizerStateBegan:            // 1. Mark the interacting flag. Used when supplying it in delegate.            self.interacting = YES;            [self.presentingVC dismissViewControllerAnimated:YES completion:nil];            break;        case UIGestureRecognizerStateChanged: {            // 2. Calculate the percentage of guesture            CGFloat fraction = translation.y / 400.0;            //Limit it between 0 and 1            fraction = fminf(fmaxf(fraction, 0.0), 1.0);            self.shouldComplete = (fraction > 0.5);            [self updateInteractiveTransition:fraction];            break;        }        case UIGestureRecognizerStateEnded:        case UIGestureRecognizerStateCancelled: {            // 3. Gesture over. Check if the transition should happen or not            self.interacting = NO;            if (!self.shouldComplete || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {                [self cancelInteractiveTransition];            } else {                [self finishInteractiveTransition];            }            break;        }        default:            break;    }}@end

            有點長,但是做的事情還是比較簡單的。

            1. 我們設定了一個BOOL變數來表示是否處於切換過程中。這個布爾值將在監測到手勢開始時被設定,我們之後會在調用返回這個InteractiveTransition的時候用到。
            2. 計算百分比,我們設定了向下划動400像素或以上為100%,每次手勢狀態變化時根據當前手勢位置計算新的百分比,結果被限制在0~1之間。然後更新InteractiveTransition的百分數。
            3. 手勢結束時,把正在切換的標設定回NO,然後進行判斷。在2中我們設定了手勢距離超過設定一半就認為應該結束手勢,否則就應該返回原來狀態。在這裡使用其進行判斷,已決定這次transition是否應該結束。

              接下來我們需要添加一個向下移動的UIView動畫,用來表現dismiss。這個十分簡單,和BouncePresentAnimation很相似,寫一個NormalDismissAnimation的實現了UIViewControllerAnimatedTransitioning介面的類就可以了,本文裡略過不寫了,感興趣的童鞋可以自行查看源碼。

              最後調整MainViewController的內容,主要修改點有三個地方:

              //MainViewController.m@interface MainViewController ()//...// 1. Add dismiss animation and transition controller@property (nonatomic, strong) NormalDismissAnimation *dismissAnimation;@property (nonatomic, strong) SwipeUpInteractiveTransition *transitionController;@end@implementation MainViewController- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{     //...        _dismissAnimation = [NormalDismissAnimation new];        _transitionController = [SwipeUpInteractiveTransition new];    //...}-(void) buttonClicked:(id)sender{    //...    // 2. Bind current VC to transition controller.    [self.transitionController wireToViewController:mvc];    //...}// 3. Implement the methods to supply proper objects.-(id)animationControllerForDismissedController:(UIViewController *)dismissed{    return self.dismissAnimation;}-(id)interactionControllerForDismissal:(id)animator {    return self.transitionController.interacting ? self.transitionController : nil;}
              1. 在其中添加dismiss時候的動畫和互動切換Controller
              2. 在初始化modalVC的時候為互動切換的Controller綁定VC
              3. 為UIViewControllerTransitioningDelegate實現dismiss時候的委託方法,包括返回對應的動畫以及互動切換Controller

                完成了,如果向下划動時,效果如下:

                關於iOS 7中自訂VC切換的一些總結

                demo中只展示了對於modalVC的present和dismiss的自訂切換開關效果,當然對與Navigation Controller的Push和Pop切換也是有相應的一套方法的。實現起來和dismiss十分類似,只不過對應UIViewControllerTransitioningDelegate的詢問動畫和互動的方法換到了UINavigationControllerDelegate中(為了區別push或者pop,看一下這個介面應該能馬上知道)。另外一個很好的福利是,對於標準的navController的Pop操作,蘋果已經替我們實現了手勢驅動返回,我們不用再費心每個去實現一遍了,cheers~

                另外,可能你會覺得使用VC容器其提供的transition動畫方法來進行VC切換就已經夠好夠方便了,為什麼iOS7中還要引入一套自訂的方式呢。其實從根本來說它們所承擔的是兩類完全不同的任務:自訂VC容器可以提供自己定義的VC結構,並保證系統的各類方法和通知能夠準確傳遞到合適的VC,它提供的transition方法雖然可以實現一些簡單的UIView動畫,但是難以重用,可以說是和containerVC完全耦合在一起的;而自訂切換開關並不改變VC的組織圖,只是負責提供view的效果,因為VC切換將動畫部分、動畫驅動部分都使用介面的方式給出,因此重用性非常優秀。在絕大多數情況下,精心編寫的一套UIView動畫是可以輕易地用在不同的VC中,甚至是不同的項目中的。

                需要特別一提的是,Github上的ColinEberhardt的VCTransitionsLibrary已經為我們提供了一系列的VC自訂切換開關動畫效果,正是得益於iOS7中這一塊的良好設計(雖然這幾個介面的命名比較相似,在弄明白之前會有些confusing),因此這些效果使用起來非常方便,相信一般項目中是足夠使用的了。而其他更複雜或者炫目的效果,亦可在其基礎上進行擴充改進得到。可以說隨著越來越多的應用轉向iOS7,自訂VC切換將成為新的使用者互動實現的基礎和重要部分,對於今後會在其基礎上會衍生出怎樣讓人眼前一亮的互動設計,不妨讓我們拭目以待(或者自己努力去創造)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.