此文章來自王巍大神的WWDC 2013 Session筆記 - iOS7中的ViewController切換
毫無疑問,ViewController(在本文中簡寫為VC)是使用MVC構(gòu)建Cocoa或者CocoaTouch程序時最重要的一個類,我們的日常工作中一般來說最花費時間和精力的也是在為VC部分編寫代碼。蘋果產(chǎn)品是注重用戶體驗的,而對細節(jié)進行琢磨也是蘋果對于開發(fā)者一直以來的要求和希望。在用戶體驗中,VC之間的關(guān)系,比如不同VC之間遷移和轉(zhuǎn)換動畫效果一直是一個值得不斷推敲的重點。在iOS7中,蘋果給出了一套完整的VC制作之間遷移效果的方案,可以說是為現(xiàn)在這部分各種不同實現(xiàn)方案指出了一條推薦的統(tǒng)一道路。
一: iOS 7 自定義ViewController動畫切換
自定義動畫切換的相關(guān)的主要API
在深入之前,我們先來看看新SDK中有關(guān)這部分內(nèi)容的相關(guān)接口以及它們的關(guān)系和典型用法。這幾個接口和類的名字都比較相似,但是還是能比較好的描述出各自的職能的,一開始的話可能比較迷惑,但是當(dāng)自己動手實現(xiàn)一兩個例子之后,它們之間的關(guān)系就會逐漸明晰起來。(相關(guān)的內(nèi)容都定義在UIKit的UIViewControllerTransitioning.h中了)
1.1: UIViewControllerContextTransitioning協(xié)議
這個接口用來 提供切換上下文給開發(fā)者使用,包含了從哪個VC到哪個VC等各類信息, 一般不需要開發(fā)者自己實現(xiàn)。具體來說,iOS7的自定義切換目的之一就是切換相關(guān)代碼解耦,在進行VC切換時,做切換效果實現(xiàn)的時候必須要需要切換前后VC的一些信息,系統(tǒng)在新加入的API的比較的地方都會提供一個實現(xiàn)了該接口的對象,以供我們使用。
對于切換的動畫實現(xiàn)來說
-
-(UIView *)containerViewVC切換所發(fā)生的view容器,開發(fā)者應(yīng)該將切出的view移除,將切入的view加入到該view容器中。 -
-(UIViewController *)viewControllerForKey:(NSString *)key提供一個key,返回對應(yīng)的VC?,F(xiàn)在的SDK中key的選擇只有UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey兩種,分別表示 將要切出和切入的VC -
-(CGRect)initialFrameForViewController:(UIViewController *)vc某個VC的初始位置,可以用來做動畫的計算。 -
-(CGRect)finalFrameForViewController:(UIViewController *)vc與上面的方法對應(yīng),得到切換結(jié)束時某個VC應(yīng)在的frame。 -
-(void)completeTransition:(BOOL)didComplete向這個context報告切換已經(jīng)完成
1.2: UIViewControllerAnimatedTransitioning協(xié)議
這個接口 負責(zé)切換的具體內(nèi)容,也即“切換中應(yīng)該發(fā)生什么”。開發(fā)者在做自定義切換效果時大部分代碼會是用來實現(xiàn)這個接口。它只有兩個方法需要我們實現(xiàn):
-
-(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext系統(tǒng)給出一個切換上下文,我們根據(jù)上下文環(huán)境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,SDK會用這個時間來在百分比驅(qū)動的切換中進行幀的計算,后面再詳細展開)。 -
-(void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext在進行切換的時候?qū)⒄{(diào)用該方法,我們對于切換時的UIView的設(shè)置和動畫都在這個方法中完成。
1.3: UIViewControllerTransitioningDelegate
這個接口的作用比較簡單單一,在需要VC切換的時候系統(tǒng)會像實現(xiàn)了這個接口的對象詢問是否需要使用自定義的切換效果。這個接口共有四個類似的方法:
- -(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;
前兩個方法是針對動畫切換的,我們需要分別在呈現(xiàn)VC和解散VC時,給出一個實現(xiàn)了UIViewControllerAnimatedTransitioning接口的對象(其中包含切換時長和如何切換)。后兩個方法涉及交互式切換,之后再說。
二: Demo
還是那句話,一百行的講解不如一個簡單的小Demo,于是..it’s demo time~ 整個demo的代碼我放到了github的這個頁面上,有需要的朋友可以參照著看這篇文章。
我們打算做一個簡單的自定義的modalViewController的切換效果。普通的present modalVC的效果大家都已經(jīng)很熟悉了,這次我們先實現(xiàn)一個自定義的類似的modal present的效果,與普通效果不同的是,我們希望modalVC出現(xiàn)的時候不要那么乏味的就簡單從底部出現(xiàn),而是帶有一個彈性效果(這里雖然是彈性,但是僅指使用UIView的模擬動畫。我們首先實現(xiàn)簡單的ModalVC彈出吧..這段非?;A(chǔ)
2.1: 先定義一個ModalVC,以及相應(yīng)的protocal和delegate方法:
//ModalViewController.h
@class ModalViewController;
@protocol ModalViewControllerDelegate <NSObject>
-(void) modalViewControllerDidClickedDismissButton:(ModalViewController *)viewController;
@end
@interface ModalViewController : UIViewController
@property (nonatomic, weak) id<ModalViewControllerDelegate> 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的實現(xiàn)方式了。需要多嘴一句的是,在實際使用中有的同學(xué)喜歡在-buttonClicked:中直接給self發(fā)送dismissViewController的相關(guān)方法。在現(xiàn)在的SDK中,如果當(dāng)前的VC是被顯示的話,這個消息會被直接轉(zhuǎn)發(fā)到顯示它的VC去。但是這并不是一個好的實現(xiàn),違反了程序設(shè)計的哲學(xué),也很容易掉到坑里,具體案例可以參看這篇文章的評論。
2.2: 所以我們用標準的方式來呈現(xiàn)和解散這個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];
}
測試一下,沒問題,然后我們可以開始實現(xiàn)自定義的切換效果了。首先我們需要一個實現(xiàn)了
2.3: UIViewControllerAnimatedTransitioning的對象,新建一個類來實現(xiàn)吧,比如BouncePresentAnimation
//BouncePresentAnimation.h
@interface BouncePresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>
@end
//BouncePresentAnimation.m
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.8f;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)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];
}];
}
解釋一下這個實現(xiàn):
- 1: 我們首先需要得到參與切換的兩個
ViewController的信息,使用context的方法拿到它們的參照; - 2: 對于要呈現(xiàn)的VC,我們希望它從屏幕下方出現(xiàn),因此將初始位置設(shè)置到屏幕下邊緣;
- 3: 將view添加到
containerView中; - 4: 開始動畫。這里的動畫時間長度和切換時間長度一致,都為0.8s。usingSpringWithDamping的UIView動畫API是iOS7新加入的,描述了一個模擬彈簧動作的動畫曲線,我們在這里只做使用,更多信息可以參看相關(guān)文檔;(順便多說一句,iOS7中對UIView動畫添加了一個很方便的Category,UIViewKeyframeAnimations。使用其中方法可以為UIView動畫添加關(guān)鍵幀動畫)
- 5: 在動畫結(jié)束后我們必須向
context報告VC切換完成,是否成功(在這里的動畫切換中,沒有失敗的可能性,因此直接pass一個YES過去)。系統(tǒng)在接收到這個消息后,將對VC狀態(tài)進行維護。
2.4: 實現(xiàn)一個UIViewControllerTransitioningDelegate,
應(yīng)該就能讓它工作了。簡單來說,一個比較好的地方是直接在MainViewController中實現(xiàn)這個接口。在MainVC中聲明實現(xiàn)這個接口,然后加入或變更為如下代碼:
@interface MainViewController ()<ModalViewControllerDelegate, UIViewControllerTransitioningDelegate>
@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 <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
return self.presentAnimation;
}
Believe or not, we have done. 跑一下,應(yīng)該可以得到如下效果:

三: 手勢驅(qū)動的百分比切換
iOS7引入了一種手勢驅(qū)動的VC切換的方式(交互式切換)。如果你使用系統(tǒng)的各種應(yīng)用,在navViewController里push了一個新的VC的話,返回時并不需要點擊左上的Back按鈕,而是通過從屏幕左側(cè)劃向右側(cè)即可完成返回操作。而在這個操作過程中,我們甚至可以撤銷我們的手勢,以取消這次VC轉(zhuǎn)移。在新版的Safari中,我們甚至可以用相同的手勢來完成網(wǎng)頁的后退功能(所以很大程度上來說屏幕底部的工具欄成為了擺設(shè))。如果您還不知道或者沒太留意過這個改動,不妨現(xiàn)在就拿手邊的iOS7這輩試試看,手機瀏覽的朋友記得切回來哦 :)
我們這就動手在自己的VC切換中實現(xiàn)這個功能吧,首先我們需要在剛才的知識基礎(chǔ)上補充一些東西:
首先是UIViewControllerContextTransitioning,剛才提到這個是系統(tǒng)提供的VC切換上下文,如果您深入看了它的頭文件描述的話,應(yīng)該會發(fā)現(xiàn)其中有三個關(guān)于InteractiveTransition的方法,正是用來處理交互式切換的。但是在初級的實際使用中我們其實可以不太理會它們,而是使用iOS 7 SDK已經(jīng)給我們準備好的一個現(xiàn)成轉(zhuǎn)為交互式切換而新加的類:UIPercentDrivenInteractiveTransition。
3.1: UIPercentDrivenInteractiveTransition是什么
這是一個實現(xiàn)了UIViewControllerInteractiveTransitioning接口的類,為我們預(yù)先實現(xiàn)和提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。一般來說我們更多地會使用某些手勢來完成交互式的轉(zhuǎn)移(當(dāng)然用的高級的話用其他的輸入..比如聲音,iBeacon距離或者甚至面部微笑來做輸入驅(qū)動也無不可,畢竟想象無極限嘛..),這樣使用這個類(一般是其子類)的話就會非常方便。我們在手勢識別中只需要告訴這個類的實例當(dāng)前的狀態(tài)百分比如何,系統(tǒng)便根據(jù)這個百分比和我們之前設(shè)定的遷移方式為我們計算當(dāng)前應(yīng)該的UI渲染,十分方便。具體的幾個重要方法:
-
-(void)updateInteractiveTransition:(CGFloat)percentComplete更新百分比,一般通過手勢識別的長度之類的來計算一個值,然后進行更新。之后的例子里會看到詳細的用法 -
-(void)cancelInteractiveTransition報告交互取消,返回切換前的狀態(tài) -
–(void)finishInteractiveTransition報告交互完成,更新到切換后的狀態(tài)
3.2: UIViewControllerInteractiveTransitioning協(xié)議
就如上面提到的,UIPercentDrivenInteractiveTransition只是實現(xiàn)了這個接口的一個類。為了實現(xiàn)交互式切換的功能,我們需要實現(xiàn)這個接口。因為大部分時候我們其實不需要自己來實現(xiàn)這個接口,因此在這篇入門中就不展開說明了,有興趣的童鞋可以自行鉆研。
還有就是上面提到過的UIViewControllerTransitioningDelegate中的返回Interactive實現(xiàn)對象的方法,我們同樣會在交互式切換中用到它們。
四: 繼續(xù)Demo
Demo time again。在剛才demo的基礎(chǔ)上,這次我們用一個向上劃動的手勢來吧之前呈現(xiàn)的ModalViewController給dismiss掉~當(dāng)然是交互式的切換,可以半途取消的那種。
首先新建一個類,繼承自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
- 我們設(shè)定了一個BOOL變量來表示是否處于切換過程中。這個布爾值將在監(jiān)測到手勢開始時被設(shè)置,我們之后會在調(diào)用返回這個
InteractiveTransition的時候用到。 - 計算百分比,我們設(shè)定了向下劃動400像素或以上為100%,每次手勢狀態(tài)變化時根據(jù)當(dāng)前手勢位置計算新的百分比,結(jié)果被限制在0~1之間。然后更新
InteractiveTransition的百分數(shù)。 - 手勢結(jié)束時,把正在切換的標設(shè)置回NO,然后進行判斷。在2中我們設(shè)定了手勢距離超過設(shè)定一半就認為應(yīng)該結(jié)束手勢,否則就應(yīng)該返回原來狀態(tài)。在這里使用其進行判斷,已決定這次transition是否應(yīng)該結(jié)束
接下來我們需要添加一個向下移動的UIView動畫,用來表現(xiàn)dismiss。這個十分簡單,和BouncePresentAnimation很相似,寫一個NormalDismissAnimation的實現(xiàn)了UIViewControllerAnimatedTransitioning接口的類就可以了,本文里略過不寫了,感興趣的童鞋可以自行查看源碼。
最后調(diào)整MainViewController的內(nèi)容,主要修改點有三個地方:
//MainViewController.m
@interface MainViewController ()<ModalViewControllerDelegate,UIViewControllerTransitioningDelegate>
//...
// 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<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
return self.dismissAnimation;
}
-(id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
return self.transitionController.interacting ? self.transitionController : nil;
}

五: 關(guān)于iOS 7中自定義VC切換的一些總結(jié)
demo中只展示了對于modalVC的present和dismiss的自定義切換效果,當(dāng)然對與Navigation Controller的Push和Pop切換也是有相應(yīng)的一套方法的。實現(xiàn)起來和dismiss十分類似,只不過對應(yīng)UIViewControllerTransitioningDelegate的詢問動畫和交互的方法換到了UINavigationControllerDelegate中(為了區(qū)別push或者pop,看一下這個接口應(yīng)該能馬上知道)。另外一個很好的福利是,對于標準的navController的Pop操作,蘋果已經(jīng)替我們實現(xiàn)了手勢驅(qū)動返回,我們不用再費心每個去實現(xiàn)一遍了,cheers~
另外,可能你會覺得使用VC容器其提供的transition動畫方法來進行VC切換就已經(jīng)夠好夠方便了,為什么iOS7中還要引入一套自定義的方式呢。其實從根本來說它們所承擔(dān)的是兩類完全不同的任務(wù):自定義VC容器可以提供自己定義的VC結(jié)構(gòu),并保證系統(tǒng)的各類方法和通知能夠準確傳遞到合適的VC,它提供的transition方法雖然可以實現(xiàn)一些簡單的UIView動畫,但是難以重用,可以說是和containerVC完全耦合在一起的;而自定義切換并不改變VC的組織結(jié)構(gòu),只是負責(zé)提供view的效果,因為VC切換將動畫部分、動畫驅(qū)動部分都使用接口的方式給出,因此重用性非常優(yōu)秀。在絕大多數(shù)情況下,精心編寫的一套UIView動畫是可以輕易地用在不同的VC中,甚至是不同的項目中的。
需要特別一提的是,Github上的ColinEberhardt的VCTransitionsLibrary已經(jīng)為我們提供了一系列的VC自定義切換動畫效果,正是得益于iOS7中這一塊的良好設(shè)計(雖然這幾個接口的命名比較相似,在弄明白之前會有些confusing),因此這些效果使用起來非常方便,相信一般項目中是足夠使用的了。而其他更復(fù)雜或者炫目的效果,亦可在其基礎(chǔ)上進行擴展改進得到??梢哉f隨著越來越多的應(yīng)用轉(zhuǎn)向iOS7,自定義VC切換將成為新的用戶交互實現(xiàn)的基礎(chǔ)和重要部分,對于今后會在其基礎(chǔ)上會衍生出怎樣讓人眼前一亮的交互設(shè)計,不妨讓我們拭目以待(或者自己努力去創(chuàng)造)。