2 // ZFModalTransitionAnimator.m
4 // Created by Amornchai Kanokpullwad on 5/10/14.
5 // Copyright (c) 2014 zoonref. All rights reserved.
8 #import "ZFModalTransitionAnimator.h"
10 @interface ZFModalTransitionAnimator ()
11 @property (nonatomic, weak) UIViewController *modalController;
12 @property (nonatomic, strong) ZFDetectScrollViewEndGestureRecognizer *gesture;
13 @property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext;
14 @property CGFloat panLocationStart;
15 @property BOOL isDismiss;
16 @property BOOL isInteractive;
17 @property CATransform3D tempTransform;
20 @implementation ZFModalTransitionAnimator
22 - (instancetype)initWithModalViewController:(UIViewController *)modalViewController
26 _modalController = modalViewController;
27 _direction = ZFModalTransitonDirectionBottom;
30 _behindViewScale = 0.9f;
31 _behindViewAlpha = 1.0f;
32 _transitionDuration = 0.8f;
34 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
35 [[NSNotificationCenter defaultCenter] addObserver:self
36 selector:@selector(orientationChanged:)
37 name:UIApplicationDidChangeStatusBarFrameNotification
45 [[NSNotificationCenter defaultCenter] removeObserver:self];
46 [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
49 - (void)setDragable:(BOOL)dragable
53 [self removeGestureRecognizerFromModalController];
54 self.gesture = [[ZFDetectScrollViewEndGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
55 self.gesture.delegate = self;
56 [self.modalController.view addGestureRecognizer:self.gesture];
58 [self removeGestureRecognizerFromModalController];
62 - (void)setContentScrollView:(UIScrollView *)scrollView
64 // always enable drag if scrollview is set
68 // and scrollview will work only for bottom mode
69 self.direction = ZFModalTransitonDirectionBottom;
70 self.gesture.scrollview = scrollView;
73 - (void)setDirection:(ZFModalTransitonDirection)direction
75 _direction = direction;
76 // scrollview will work only for bottom mode
77 if (_direction != ZFModalTransitonDirectionBottom) {
78 self.gesture.scrollview = nil;
82 - (void)animationEnded:(BOOL)transitionCompleted
84 // Reset to our default state
85 self.isInteractive = NO;
86 self.transitionContext = nil;
89 - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
91 return self.transitionDuration;
94 - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
96 if (self.isInteractive) {
99 // Grab the from and to view controllers from the context
100 UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
101 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
103 UIView *containerView = [transitionContext containerView];
105 if (!self.isDismiss) {
109 [containerView addSubview:toViewController.view];
111 toViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
113 if (self.direction == ZFModalTransitonDirectionBottom) {
114 startRect = CGRectMake(0,
115 CGRectGetHeight(containerView.frame),
116 CGRectGetWidth(containerView.bounds),
117 CGRectGetHeight(containerView.bounds));
118 } else if (self.direction == ZFModalTransitonDirectionLeft) {
119 startRect = CGRectMake(-CGRectGetWidth(containerView.frame),
121 CGRectGetWidth(containerView.bounds),
122 CGRectGetHeight(containerView.bounds));
123 } else if (self.direction == ZFModalTransitonDirectionRight) {
124 startRect = CGRectMake(CGRectGetWidth(containerView.frame),
126 CGRectGetWidth(containerView.bounds),
127 CGRectGetHeight(containerView.bounds));
130 CGPoint transformedPoint = CGPointApplyAffineTransform(startRect.origin, toViewController.view.transform);
131 toViewController.view.frame = CGRectMake(transformedPoint.x, transformedPoint.y, startRect.size.width, startRect.size.height);
133 if (toViewController.modalPresentationStyle == UIModalPresentationCustom) {
134 [fromViewController beginAppearanceTransition:NO animated:YES];
137 [UIView animateWithDuration:[self transitionDuration:transitionContext]
139 usingSpringWithDamping:0.8
140 initialSpringVelocity:0.1
141 options:UIViewAnimationOptionCurveEaseOut
143 fromViewController.view.transform = CGAffineTransformScale(fromViewController.view.transform, self.behindViewScale, self.behindViewScale);
144 fromViewController.view.alpha = self.behindViewAlpha;
146 toViewController.view.frame = CGRectMake(0,0,
147 CGRectGetWidth(toViewController.view.frame),
148 CGRectGetHeight(toViewController.view.frame));
149 } completion:^(BOOL finished) {
150 if (toViewController.modalPresentationStyle == UIModalPresentationCustom) {
151 [fromViewController endAppearanceTransition];
153 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
157 if (fromViewController.modalPresentationStyle == UIModalPresentationFullScreen) {
158 [containerView addSubview:toViewController.view];
161 [containerView bringSubviewToFront:fromViewController.view];
163 if (![self isPriorToIOS8]) {
164 toViewController.view.layer.transform = CATransform3DScale(toViewController.view.layer.transform, self.behindViewScale, self.behindViewScale, 1);
167 toViewController.view.alpha = self.behindViewAlpha;
171 if (self.direction == ZFModalTransitonDirectionBottom) {
172 endRect = CGRectMake(0,
173 CGRectGetHeight(fromViewController.view.bounds),
174 CGRectGetWidth(fromViewController.view.frame),
175 CGRectGetHeight(fromViewController.view.frame));
176 } else if (self.direction == ZFModalTransitonDirectionLeft) {
177 endRect = CGRectMake(-CGRectGetWidth(fromViewController.view.bounds),
179 CGRectGetWidth(fromViewController.view.frame),
180 CGRectGetHeight(fromViewController.view.frame));
181 } else if (self.direction == ZFModalTransitonDirectionRight) {
182 endRect = CGRectMake(CGRectGetWidth(fromViewController.view.bounds),
184 CGRectGetWidth(fromViewController.view.frame),
185 CGRectGetHeight(fromViewController.view.frame));
188 CGPoint transformedPoint = CGPointApplyAffineTransform(endRect.origin, fromViewController.view.transform);
189 endRect = CGRectMake(transformedPoint.x, transformedPoint.y, endRect.size.width, endRect.size.height);
191 if (fromViewController.modalPresentationStyle == UIModalPresentationCustom) {
192 [toViewController beginAppearanceTransition:YES animated:YES];
195 [UIView animateWithDuration:[self transitionDuration:transitionContext]
197 usingSpringWithDamping:0.8
198 initialSpringVelocity:0.1
199 options:UIViewAnimationOptionCurveEaseOut
201 CGFloat scaleBack = (1 / self.behindViewScale);
202 toViewController.view.layer.transform = CATransform3DScale(toViewController.view.layer.transform, scaleBack, scaleBack, 1);
203 toViewController.view.alpha = 1.0f;
204 fromViewController.view.frame = endRect;
205 } completion:^(BOOL finished) {
206 toViewController.view.layer.transform = CATransform3DIdentity;
207 if (fromViewController.modalPresentationStyle == UIModalPresentationCustom) {
208 [toViewController endAppearanceTransition];
210 [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
215 - (void)removeGestureRecognizerFromModalController
217 if (self.gesture && [self.modalController.view.gestureRecognizers containsObject:self.gesture]) {
218 [self.modalController.view removeGestureRecognizer:self.gesture];
223 # pragma mark - Gesture
225 - (void)handlePan:(UIPanGestureRecognizer *)recognizer
227 // Location reference
228 CGPoint location = [recognizer locationInView:self.modalController.view.window];
229 location = CGPointApplyAffineTransform(location, CGAffineTransformInvert(recognizer.view.transform));
230 // Velocity reference
231 CGPoint velocity = [recognizer velocityInView:[self.modalController.view window]];
232 velocity = CGPointApplyAffineTransform(velocity, CGAffineTransformInvert(recognizer.view.transform));
234 if (recognizer.state == UIGestureRecognizerStateBegan) {
235 self.isInteractive = YES;
236 if (self.direction == ZFModalTransitonDirectionBottom) {
237 self.panLocationStart = location.y;
239 self.panLocationStart = location.x;
241 [self.modalController dismissViewControllerAnimated:YES completion:nil];
243 } else if (recognizer.state == UIGestureRecognizerStateChanged) {
244 CGFloat animationRatio = 0;
246 if (self.direction == ZFModalTransitonDirectionBottom) {
247 animationRatio = (location.y - self.panLocationStart) / (CGRectGetHeight([self.modalController view].bounds));
248 } else if (self.direction == ZFModalTransitonDirectionLeft) {
249 animationRatio = (self.panLocationStart - location.x) / (CGRectGetWidth([self.modalController view].bounds));
250 } else if (self.direction == ZFModalTransitonDirectionRight) {
251 animationRatio = (location.x - self.panLocationStart) / (CGRectGetWidth([self.modalController view].bounds));
254 [self updateInteractiveTransition:animationRatio];
256 } else if (recognizer.state == UIGestureRecognizerStateEnded) {
258 CGFloat velocityForSelectedDirection;
260 if (self.direction == ZFModalTransitonDirectionBottom) {
261 velocityForSelectedDirection = velocity.y;
263 velocityForSelectedDirection = velocity.x;
266 if (velocityForSelectedDirection > 100
267 && (self.direction == ZFModalTransitonDirectionRight
268 || self.direction == ZFModalTransitonDirectionBottom)) {
269 [self finishInteractiveTransition];
270 } else if (velocityForSelectedDirection < -100 && self.direction == ZFModalTransitonDirectionLeft) {
271 [self finishInteractiveTransition];
273 [self cancelInteractiveTransition];
275 self.isInteractive = NO;
281 -(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
283 self.transitionContext = transitionContext;
285 UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
286 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
288 if (![self isPriorToIOS8]) {
289 toViewController.view.layer.transform = CATransform3DScale(toViewController.view.layer.transform, self.behindViewScale, self.behindViewScale, 1);
292 self.tempTransform = toViewController.view.layer.transform;
294 toViewController.view.alpha = self.behindViewAlpha;
296 if (fromViewController.modalPresentationStyle == UIModalPresentationFullScreen) {
297 [[transitionContext containerView] addSubview:toViewController.view];
299 [[transitionContext containerView] bringSubviewToFront:fromViewController.view];
302 - (void)updateInteractiveTransition:(CGFloat)percentComplete
304 if (!self.bounces && percentComplete < 0) {
308 id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
310 UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
311 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
312 CATransform3D transform = CATransform3DMakeScale(
313 1 + (((1 / self.behindViewScale) - 1) * percentComplete),
314 1 + (((1 / self.behindViewScale) - 1) * percentComplete), 1);
315 toViewController.view.layer.transform = CATransform3DConcat(self.tempTransform, transform);
317 toViewController.view.alpha = self.behindViewAlpha + ((1 - self.behindViewAlpha) * percentComplete);
320 if (self.direction == ZFModalTransitonDirectionBottom) {
321 updateRect = CGRectMake(0,
322 (CGRectGetHeight(fromViewController.view.bounds) * percentComplete),
323 CGRectGetWidth(fromViewController.view.frame),
324 CGRectGetHeight(fromViewController.view.frame));
325 } else if (self.direction == ZFModalTransitonDirectionLeft) {
326 updateRect = CGRectMake(-(CGRectGetWidth(fromViewController.view.bounds) * percentComplete),
328 CGRectGetWidth(fromViewController.view.frame),
329 CGRectGetHeight(fromViewController.view.frame));
330 } else if (self.direction == ZFModalTransitonDirectionRight) {
331 updateRect = CGRectMake(CGRectGetWidth(fromViewController.view.bounds) * percentComplete,
333 CGRectGetWidth(fromViewController.view.frame),
334 CGRectGetHeight(fromViewController.view.frame));
337 // reset to zero if x and y has unexpected value to prevent crash
338 if (isnan(updateRect.origin.x) || isinf(updateRect.origin.x)) {
339 updateRect.origin.x = 0;
341 if (isnan(updateRect.origin.y) || isinf(updateRect.origin.y)) {
342 updateRect.origin.y = 0;
345 CGPoint transformedPoint = CGPointApplyAffineTransform(updateRect.origin, fromViewController.view.transform);
346 updateRect = CGRectMake(transformedPoint.x, transformedPoint.y, updateRect.size.width, updateRect.size.height);
348 fromViewController.view.frame = updateRect;
351 - (void)finishInteractiveTransition
353 id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
355 UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
356 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
360 if (self.direction == ZFModalTransitonDirectionBottom) {
361 endRect = CGRectMake(0,
362 CGRectGetHeight(fromViewController.view.bounds),
363 CGRectGetWidth(fromViewController.view.frame),
364 CGRectGetHeight(fromViewController.view.frame));
365 } else if (self.direction == ZFModalTransitonDirectionLeft) {
366 endRect = CGRectMake(-CGRectGetWidth(fromViewController.view.bounds),
368 CGRectGetWidth(fromViewController.view.frame),
369 CGRectGetHeight(fromViewController.view.frame));
370 } else if (self.direction == ZFModalTransitonDirectionRight) {
371 endRect = CGRectMake(CGRectGetWidth(fromViewController.view.bounds),
373 CGRectGetWidth(fromViewController.view.frame),
374 CGRectGetHeight(fromViewController.view.frame));
377 CGPoint transformedPoint = CGPointApplyAffineTransform(endRect.origin, fromViewController.view.transform);
378 endRect = CGRectMake(transformedPoint.x, transformedPoint.y, endRect.size.width, endRect.size.height);
380 if (fromViewController.modalPresentationStyle == UIModalPresentationCustom) {
381 [toViewController beginAppearanceTransition:YES animated:YES];
384 [UIView animateWithDuration:[self transitionDuration:transitionContext]
386 usingSpringWithDamping:0.8
387 initialSpringVelocity:0.1
388 options:UIViewAnimationOptionCurveEaseOut
390 CGFloat scaleBack = (1 / self.behindViewScale);
391 toViewController.view.layer.transform = CATransform3DScale(self.tempTransform, scaleBack, scaleBack, 1);
392 toViewController.view.alpha = 1.0f;
393 fromViewController.view.frame = endRect;
394 } completion:^(BOOL finished) {
395 if (fromViewController.modalPresentationStyle == UIModalPresentationCustom) {
396 [toViewController endAppearanceTransition];
398 [transitionContext completeTransition:YES];
402 - (void)cancelInteractiveTransition
404 id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
406 UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
407 UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
409 [UIView animateWithDuration:0.4
411 usingSpringWithDamping:0.8
412 initialSpringVelocity:0.1
413 options:UIViewAnimationOptionCurveEaseOut
415 toViewController.view.layer.transform = self.tempTransform;
416 toViewController.view.alpha = self.behindViewAlpha;
418 fromViewController.view.frame = CGRectMake(0,0,
419 CGRectGetWidth(fromViewController.view.frame),
420 CGRectGetHeight(fromViewController.view.frame));
421 } completion:^(BOOL finished) {
422 [transitionContext completeTransition:NO];
423 if (fromViewController.modalPresentationStyle == UIModalPresentationFullScreen) {
424 [toViewController.view removeFromSuperview];
429 #pragma mark - UIViewControllerTransitioningDelegate Methods
431 - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
437 - (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
439 self.isDismiss = YES;
443 - (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator
448 - (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator
450 // Return nil if we are not interactive
451 if (self.isInteractive && self.dragable) {
452 self.isDismiss = YES;
459 #pragma mark - Gesture Delegate
461 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
463 if (self.direction == ZFModalTransitonDirectionBottom) {
469 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
471 if (self.direction == ZFModalTransitonDirectionBottom) {
477 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
478 if (self.gestureRecognizerToFailPan && otherGestureRecognizer && self.gestureRecognizerToFailPan == otherGestureRecognizer) {
487 - (BOOL)isPriorToIOS8
489 NSComparisonResult order = [[UIDevice currentDevice].systemVersion compare: @"8.0" options: NSNumericSearch];
490 if (order == NSOrderedSame || order == NSOrderedDescending) {
497 #pragma mark - Orientation
499 - (void)orientationChanged:(NSNotification *)notification
501 UIViewController *backViewController = self.modalController.presentingViewController;
502 backViewController.view.transform = CGAffineTransformIdentity;
503 backViewController.view.frame = self.modalController.view.bounds;
504 backViewController.view.transform = CGAffineTransformScale(backViewController.view.transform, self.behindViewScale, self.behindViewScale);
509 // Gesture Class Implement
510 @interface ZFDetectScrollViewEndGestureRecognizer ()
511 @property (nonatomic, strong) NSNumber *isFail;
514 @implementation ZFDetectScrollViewEndGestureRecognizer
522 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
524 [super touchesMoved:touches withEvent:event];
526 if (!self.scrollview) {
530 if (self.state == UIGestureRecognizerStateFailed) return;
531 CGPoint velocity = [self velocityInView:self.view];
532 CGPoint nowPoint = [touches.anyObject locationInView:self.view];
533 CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view];
536 if (self.isFail.boolValue) {
537 self.state = UIGestureRecognizerStateFailed;
542 CGFloat topVerticalOffset = -self.scrollview.contentInset.top;
544 if ((fabs(velocity.x) < fabs(velocity.y)) && (nowPoint.y > prevPoint.y) && (self.scrollview.contentOffset.y <= topVerticalOffset)) {
546 } else if (self.scrollview.contentOffset.y >= topVerticalOffset) {
547 self.state = UIGestureRecognizerStateFailed;