在深刻iOS 7的VC切換效果的新API實現以前,先讓咱們回顧下如今的通常作法吧。這能夠幫助理解爲何iOS7要對VC切換給出新的解決方案,若是您對iOS 5中引入的VC容器比較熟悉的話,能夠跳過這節。html
在iOS5和iOS6中,除了標準的Push,Tab和PresentModal以外,通常是使用ChildViewController的方式來完成VC之間切換的過渡效果。ChildViewController和自定義的Controller容器是iOS 5 SDK中加入的,能夠用來生成自定義的VC容器,簡單來講典型的一種用法相似這樣:ios
//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:
方法添加到容器中)。git
VC容器的主要目的是解決將不一樣VC添加到同一個屏幕上的需求,以及能夠提供一些簡單的自定義切換效果。使用VC容器可使view的關係正確,使添加的VC可以正確接收到例如屏幕旋轉,viewDidLoad:等VC事件,進而進行正確相應。VC容器確實能夠解決一部分問題,可是也應該看到,對於自定義切換效果來講,這樣的解決還有不少不足。首先是代碼高度耦合,VC切換部分的代碼直接寫在container中,難以分離重用;其次可以提供的切換效果比較有限,只能使用UIView動畫來切換,管理起來也略顯麻煩。iOS 7提供了一套新的自定義VC切換,就是針對這兩個問題的。github
在深刻以前,咱們先來看看新SDK中有關這部份內容的相關接口以及它們的關係和典型用法。這幾個接口和類的名字都比較類似,可是仍是能比較好的描述出各自的職能的,一開始的話可能比較迷惑,可是當本身動手實現一兩個例子以後,它們之間的關係就會逐漸明晰起來。(相關的內容都定義在UIKit的UIViewControllerTransitioning.h中了)mvc
這個接口用來提供切換上下文給開發者使用,包含了從哪一個VC到哪一個VC等各種信息,通常不須要開發者本身實現。具體來講,iOS7的自定義切換目的之一就是切換相關代碼解耦,在進行VC切換時,作切換效果實現的時候必需要須要切換先後VC的一些信息,系統在新加入的API的比較的地方都會提供一個實現了該接口的對象,以供咱們使用。app
對於切換的動畫實現來講(這裏先介紹簡單的動畫,在後面我會再引入手勢驅動的動畫),這個接口中最重要的方法有:iview
這個接口負責切換的具體內容,也即「切換中應該發生什麼」。開發者在作自定義切換效果時大部分代碼會是用來實現這個接口。它只有兩個方法須要咱們實現:ide
-(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 <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的實現方式了。須要多嘴一句的是,在實際使用中有的同窗喜歡在-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<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]; }]; }
解釋一下這個實現:
接下來咱們實現一個UIViewControllerTransitioningDelegate,應該就能讓它工做了。簡單來講,一個比較好的地方是直接在MainViewController中實現這個接口。在MainVC中聲明實現這個接口,而後加入或變動爲以下代碼:
@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. 跑一下,應該能夠獲得以下效果:
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 ()<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; }
完成了,若是向下划動時,效果以下:
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切換將成爲新的用戶交互實現的基礎和重要部分,對於從此會在其基礎上會衍生出怎樣讓人眼前一亮的交互設計,不妨讓咱們拭目以待(或者本身努力去創造)。