本文是我學習了onevcat的這篇轉場入門作的一點筆記。ios
今天咱們來實現一個簡單的自定義轉場,咱們先來看看這篇文章將要實現的一個效果圖吧:
框架
咱們先建立一個工程,首先用storyboard快速的建立兩個控制器,一個做爲主控制器,叫ViewController
,另一個做爲present出來的控制器,叫PresentViewController
,而且用autoLayout快速搭建好界面。就像這樣:
iview
咱們先作好點擊ViewController
上面的按鈕,present出 PresentViewController
,點擊PresentViewController
上面的按鈕,dismiss掉PresentViewController
的邏輯。這裏有兩個注意點:ide
由於此處我使用了segue
,因此在ViewController
按鈕點擊的時候,咱們只須要這樣調用就行。學習
#pragma mark - 點我彈出 -(IBAction)presentBtnClick:(UIButton *)sender { [self performSegueWithIdentifier:@"PresentSegue" sender:nil]; }
咱們平時寫dismiss的時候,通常都會是在第二個控制器中直接給self發送dismissViewController
的相關方法。在如今的SDK中,若是當前的VC是被顯示的話,這個消息會被直接轉發到顯示它的VC去。可是這並非一個好的實現,違反了程序設計的哲學,也很容易掉到坑裏。因此咱們用標準的delegate
方式實現 dismiss
。動畫
首先咱們在PresentViewController
控制器中申明一個代理方法。ui
#import <UIKit/UIKit.h> @class PresentViewController; @protocol PresentViewControllerDelegate <NSObject> - (void)dismissViewController:(PresentViewController *)viewController; @end @interface PresentViewController : UIViewController @property (nonatomic, weak) id<PresentViewControllerDelegate> delegate; @end
在button的點擊事件中,讓代理去完成關閉當前控制器的工做。this
#pragma mark - 點擊關閉 - (IBAction)closeBtnClick:(UIButton *)sender { if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) { [self.delegate dismissViewController:self]; } }
與此同時,在ViewController
中須要設置PresentViewController
的代理,而且實現代理方法:atom
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"PresentSegue"]) { PresentViewController *presetVC = segue.destinationViewController; presetVC.delegate = self; } } #pragma mark - PresentViewControllerDelegate - (void)dismissViewController:(PresentViewController *)viewController { [self dismissViewControllerAnimated:YES completion:nil]; }
OK,到這裏,咱們一個基本的轉場就完成了(這也是系統自帶的一個效果)。like this:
設計
接下來,要接觸咱們今天要講的主要內容了,咱們用iOS7中一個新的類UIViewControllerTransitioning
來實現自定義轉場。
首先咱們須要一個實現了協議名爲UIViewControllerAnimatedTransitioning
的對象。建立一個類叫作PresentAnimation
繼承於NSObject
而且實現了UIViewControllerAnimatedTransitioning
協議。(注意:須要導入UIKit框架)
@interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>
這個協議負責轉場的具體內容。開發者在作自定義切換效果時大部門代碼會是用來實現這個協議的,這個協議只有兩個方法必需要實現的:
// 返回動畫的時間 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; // 在進行切換的時候將調用該方法,咱們對於切換時的UIView的設置和動畫都在這個方法中完成。 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
實現這兩個方法
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.8f; } - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { // 1.咱們須要獲得參與切換的兩個ViewController的信息,使用context的方法拿到它們的參照; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; // 2.對於要呈現的VC,咱們但願它從屏幕下方出現,所以將初始位置設置到屏幕下邊緣; CGRect finaRect = [transitionContext finalFrameForViewController:toVC]; toVC.view.frame = CGRectOffset(finaRect, 0, [UIScreen mainScreen].bounds.size.height); // 3.將view添加到containerView中; [[transitionContext containerView] addSubview:toVC.view]; // 4.開始動畫。這裏的動畫時間長度和切換時間長度一致。usingSpringWithDamping的UIView動畫API是iOS7新加入的,描述了一個模擬彈簧動做的動畫曲線; [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{ toVC.view.frame = finaRect; } completion:^(BOOL finished) { // 5.在動畫結束後咱們必須向context報告VC切換完成,是否成功。系統在接收到這個消息後,將對VC狀態進行維護。 [transitionContext completeTransition:YES]; }]; }
注意點
UITransitionContextToViewControllerKey
與UITransitionContextFromViewControllerKey
好比從A present 出B,此時A是FromViewController
,B是ToViewController
若是從B dismiss 到A,此時A是ToViewController
,B是FromViewController
這個接口的做用比較單一,在須要VC切換的時候系統會向實現了這個接口的對象詢問是否須要使用自定義轉場效果。
因此,一個比較好的地方是直接在主控制器ViewController
中實現這個協議。
在ViewController
中完成以下代碼:
@interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate> @property (nonatomic, strong) PresentAnimation *presentAnimation; @end @implementation ViewController #pragma mark - 懶加載 - (PresentAnimation *)presentAnimation { if (!_presentAnimation) { _presentAnimation = [[PresentAnimation alloc] init]; } return _presentAnimation; } #pragma mark - UIViewControllerTransitioningDelegate - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return self.presentAnimation; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"PresentSegue"]) { PresentViewController *presetVC = segue.destinationViewController; presetVC.delegate = self; presetVC.transitioningDelegate = self; } }
如今看下咱們的效果:
相對於上面系統自帶的效果來講,咱們在present出第二個控制器的時候,帶有彈簧效果。
如今咱們增長一個功能,就是用手勢滑動來dismiss,通俗的說,就是讓present出來的那個控制器使用手勢dismiss。
建立一個類,繼承自UIPercentDrivenInteractiveTransition
#import <UIKit/UIKit.h> @interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController; @end
既然傳入了這個須要手勢dismiss的VC,咱們就須要保存一下,方便當前類在其餘地方使用,因此咱們新建一個屬性來保存這個傳入的VC。
#import "PanInteractiveTransition.h" @interface PanInteractiveTransition () @property (nonatomic, strong) UIViewController *presentVC; @end @implementation PanInteractiveTransition -(void)panToDismiss:(UIViewController *)viewController { self.presentVC = viewController; UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)]; [self.presentVC.view addGestureRecognizer:panGestR]; } #pragma mark - panGestureAction -(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:self.presentVC.view]; NSLog(@"%.2f",transition.y); switch (pan.state) { case UIGestureRecognizerStateBegan:{ [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = MIN(1.0, transition.y/300); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ if (pan.state == UIGestureRecognizerStateCancelled) { // 手勢取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
和建立PresentAnimation
同樣,咱們建立一個一個DismissAnimation
類
@interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning> @end @implementation DismissAnimation -(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.4f; } -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } @end
最後,咱們在主控制器中添加一個手勢驅動的對象,一個dismiss轉場的對象,而後懶加載。
-(PanInteractiveTransition *)paninterTransition { if (!_paninterTransition) { _paninterTransition = [[PanInteractiveTransition alloc] init]; } return _paninterTransition; } -(DismissAnimation *)dismissAnimation { if (!_dismissAnimation) { _dismissAnimation = [[DismissAnimation alloc] init]; } return _dismissAnimation; } #pragma mark - UIViewControllerTransitioningDelegate -(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissAnimation; } -(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator { return self.paninterTransition; } -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"PresentSegue"]) { // ... [self.paninterTransition panToDismiss:presetVC]; } }
此時,咱們運行程序,會發現以上代碼儘管能夠手勢驅動了,可是點擊按鈕dismiss的功能沒法使用了。這是由於若是隻是返回self.paninterTransition,那麼點擊按鈕dismiss的動畫就會失效;若是隻是返回nil,那麼手勢滑動的效果將會失效。綜上所述,咱們就得分狀況考慮。
接下來咱們就來完善一下。
給PanInteractiveTransition
添加一個屬性,表示是否處於切換過程當中(用於判斷使用的是點擊按鈕dismiss仍是手勢驅動來dismiss的)
// 是否處於切換過程當中 @property (nonatomic, assign, getter=isInteracting) BOOL interacting;
給PanInteractiveTransition
添加一個屬性,表示是否須要dismiss(用於當手勢滑動到超過指定高度以後,就會dismiss,若是沒有超過,就會還原)
@property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
修改PanInteractiveTransition
中的panGestureAction:
方法:
-(void)panGestureAction:(UIPanGestureRecognizer *)pan { CGPoint transition = [pan translationInView:pan.view]; switch (pan.state) { case UIGestureRecognizerStateBegan:{ self.interacting = YES; [self.presentVC dismissViewControllerAnimated:YES completion:nil]; } break; case UIGestureRecognizerStateChanged:{ // CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0); self.shouldComplete = (percent > 0.5); [self updateInteractiveTransition:percent]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ self.interacting = NO; // 若是下移的距離小於300或者取消都當作取消 if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) { // 手勢取消 [self cancelInteractiveTransition]; }else{ [self finishInteractiveTransition]; } } break; default: break; } }
另外還有一點,就是須要修改DismissAnimation
中的一處代碼:
-(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initRect = [transitionContext initialFrameForViewController:fromVC]; CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height); UIView *contrainerView = [transitionContext containerView]; [contrainerView addSubview:toVC.view]; [contrainerView sendSubviewToBack:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = finalRect; } completion:^(BOOL finished) { // 此處作了修改,由以前的[transitionContext completeTransition:YES]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }
ok,到此爲止,咱們的一個自定義轉場動畫就算了完成了。