毫無疑問,ViewController(在本文中簡寫為VC)是使用MVC構建Cocoa或者CocoaTouch程序時最重要的一個類,我們的日常工作中一般來說最花費時間和精力的也是在為VC部分編寫代碼。蘋果產品是注重用戶體驗的,而對細節進行琢磨也是蘋果對於開發者一直以來的要求和希望。在用戶體驗中,VC之間的關系,比如不同VC之間遷移和轉換動畫效果一直是一個值得不斷推敲的重點。在iOS7中,蘋果給出了一套完整的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切換,就是針對這兩個問題的。
在深入之前,我們先來看看新SDK中有關這部分內容的相關接口以及它們的關系和典型用法。這幾個接口和類的名字都比較相似,但是還是能比較好的描述出各自的職能的,一開始的話可能比較迷惑,但是當自己動手實現一兩個例子之後,它們之間的關系就會逐漸明晰起來。(相關的內容都定義在UIKit的UIViewControllerTransitioning.h中了)
這個接口用來提供切換上下文給開發者使用,包含了從哪個VC到哪個VC等各類信息,一般不需要開發者自己實現。具體來說,iOS7的自定義切換目的之一就是切換相關代碼解耦,在進行VC切換時,做切換效果實現的時候必須要需要切換前後VC的一些信息,系統在新加入的API的比較的地方都會提供一個實現了該接口的對象,以供我們使用。
對於切換的動畫實現來說(這裡先介紹簡單的動畫,在後面我會再引入手勢驅動的動畫),這個接口中最重要的方法有:
這個接口負責切換的具體內容,也即“切換中應該發生什麼”。開發者在做自定義切換效果時大部分代碼會是用來實現這個接口。它只有兩個方法需要我們實現:
-(NSTimeInterval)transitionDuration:(id < UIViewControllerContextTransitioning >)transitionContext; 系統給出一個切換上下文,我們根據上下文環境返回這個切換所需要的花費時間(一般就返回動畫的時間就好了,SDK會用這個時間來在百分比驅動的切換中進行幀的計算,後面再詳細展開)。
-(void)animateTransition:(id < UIViewControllerContextTransitioning >)transitionContext; 在進行切換的時候將調用該方法,我們對於切換時的UIView的設置和動畫都在這個方法中完成。
這個接口的作用比較簡單單一,在需要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,於是..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];
}];
}
解釋一下這個實現:
接下來我們實現一個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。
這是一個實現了UIViewControllerInteractiveTransitioning接口的類,為我們預先實現和提供了一系列便利的方法,可以用一個百分比來控制交互式切換的過程。一般來說我們更多地會使用某些手勢來完成交互式的轉移(當然用的高級的話用其他的輸入..比如聲音,iBeacon距離或者甚至面部微笑來做輸入驅動也無不可,畢竟想象無極限嘛..),這樣使用這個類(一般是其子類)的話就會非常方便。我們在手勢識別中只需要告訴這個類的實例當前的狀態百分比如何,系統便根據這個百分比和我們之前設定的遷移方式為我們計算當前應該的UI渲染,十分方便。具體的幾個重要方法:
就如上面提到的,UIPercentDrivenInteractiveTransition只是實現了這個接口的一個類。為了實現交互式切換的功能,我們需要實現這個接口。因為大部分時候我們其實不需要自己來實現這個接口,因此在這篇入門中就不展開說明了,有興趣的童鞋可以自行鑽研。
還有就是上面提到過的UIViewControllerTransitioningDelegate中的返回Interactive實現對象的方法,我們同樣會在交互式切換中用到它們。
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
有點長,但是做的事情還是比較簡單的。
接下來我們需要添加一個向下移動的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;
}
完成了,如果向下劃動時,效果如下:
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切換將成為新的用戶交互實現的基礎和重要部分,對於今後會在其基礎上會衍生出怎樣讓人眼前一亮的交互設計,不妨讓我們拭目以待(或者自己努力去創造)。