Skip to content

Instantly share code, notes, and snippets.

@ericallam
Created May 22, 2014 14:49
Show Gist options
  • Save ericallam/42e68408ece4d4863604 to your computer and use it in GitHub Desktop.
Save ericallam/42e68408ece4d4863604 to your computer and use it in GitHub Desktop.
Interactive Animated Transition Example
//
// AppDelegate.m
// AnimationExamplesiPhone
//
// Created by Eric Allam on 10/05/2014.
#import "AppDelegate.h"
#pragma mark - UIColor Additions
@interface UIColor (Additions)
+ (instancetype)backgroundColor;
+ (instancetype)foregroundColor;
@end
@implementation UIColor (Additions)
+ (instancetype)backgroundColor
{
return [self colorWithRed:25.0/255.0 green:163.0/255.0 blue:177.0/255.0 alpha:1.0];
}
+ (instancetype)foregroundColor
{
return [self colorWithRed:255.0/255.0 green:251.0/255.0 blue:224.0/255.0 alpha:1.0];
}
@end
#pragma mark - FinishingBehavior
// See http://www.objc.io/issue-12/interactive-animations.html for a discussion of this class
@interface FinishingBehavior : UIDynamicBehavior
@property (nonatomic) CGPoint targetPoint;
@property (nonatomic) CGPoint velocity;
- (instancetype) initWithItem:(id <UIDynamicItem>)item;
@end
@interface FinishingBehavior ()
@property (nonatomic, strong) id <UIDynamicItem> item;
@property (nonatomic, strong) UIAttachmentBehavior *attachmentBehavior;
@property (nonatomic, strong) UIDynamicItemBehavior *itemBehavior;
@end
@implementation FinishingBehavior
- (instancetype) initWithItem:(id <UIDynamicItem>)item;
{
if (self = [super init]) {
self.item = item;
[self setup];
}
return self;
}
- (void)setup {
UIAttachmentBehavior *attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.item attachedToAnchor:CGPointZero];
attachmentBehavior.frequency = 3.5;
attachmentBehavior.damping = 0.6;
attachmentBehavior.length = 0;
[self addChildBehavior:attachmentBehavior];
self.attachmentBehavior = attachmentBehavior;
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.item]];
itemBehavior.density = 100;
itemBehavior.resistance = 25;
[self addChildBehavior:itemBehavior];
self.itemBehavior = itemBehavior;
}
- (void)setTargetPoint:(CGPoint)targetPoint
{
_targetPoint = targetPoint;
self.attachmentBehavior.anchorPoint = targetPoint;
}
- (void)setVelocity:(CGPoint)velocity
{
_velocity = velocity;
CGPoint currentVelocity = [self.itemBehavior linearVelocityForItem:self.item];
CGPoint velocityDelta = CGPointMake(velocity.x - currentVelocity.x, velocity.y - currentVelocity.y);
[self.itemBehavior addLinearVelocity:velocityDelta forItem:self.item];
}
@end
#pragma mark - CustomAnimatedTransition
@interface CustomAnimatedTransition : NSObject <UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning, UIGestureRecognizerDelegate, UIDynamicAnimatorDelegate>
@property (assign, nonatomic) BOOL reversed;
@property (weak, nonatomic) id<UIViewControllerContextTransitioning> context;
@property (assign, nonatomic) CGPoint initialViewCenter;
@property (assign, nonatomic) CGFloat percentComplete;
@property (assign, nonatomic) NSTimeInterval startingTime;
@property (strong, nonatomic) CADisplayLink *displayLink;
@property (strong, nonatomic) UIDynamicAnimator *animator;
@property (strong, nonatomic) FinishingBehavior *finishingBehavior;
@property (nonatomic) UIView *view;
@property (nonatomic) UIPanGestureRecognizer *gesture;
@end
@implementation CustomAnimatedTransition
- (instancetype) init {
if (self = [super init]) {
_reversed = NO;
}
return self;
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.5;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
{
CGPoint location = [gestureRecognizer locationInView:self.context.containerView];
CALayer *presentationLayer = self.view.layer.presentationLayer;
if ([presentationLayer hitTest:location])
{
return YES;
}else{
return NO;
}
}
- (void)tick:(CADisplayLink *)link
{
NSTimeInterval elapedTime = link.timestamp - self.startingTime;
NSTimeInterval duration = 0.5;
self.percentComplete = MIN(1.0, elapedTime / duration);
[self.context updateInteractiveTransition:self.percentComplete];
}
- (void)didPan:(UIPanGestureRecognizer *)gesture
{
switch (gesture.state) {
case UIGestureRecognizerStatePossible:
break;
case UIGestureRecognizerStateBegan: {
CALayer *layer = self.view.layer.presentationLayer ?: self.view.layer;
self.view.center = layer.position;
[self.view.layer removeAllAnimations];
self.initialViewCenter = self.view.center;
[self.animator removeAllBehaviors];
break;
}
case UIGestureRecognizerStateChanged:{
CGPoint translation = [gesture translationInView:gesture.view];
CGPoint centerTranslated = self.initialViewCenter;
centerTranslated.x += translation.x;
centerTranslated.y += translation.y;
self.view.center = centerTranslated;
CGFloat percentComplete = MAX(MIN(self.view.center.y / 284, 1.0), 0.0);
if (self.reversed) percentComplete = 1.0f - percentComplete;
[self.context updateInteractiveTransition:percentComplete];
break;
}
case UIGestureRecognizerStateEnded:{
CGPoint velocity = [gesture velocityInView:gesture.view];
CGPoint location = [gesture locationInView:gesture.view];
static const CGFloat kTransitionGestureVelocityThreshold = 50.0f;
static const CGFloat kTransitionGestureLocationThreshold = 284.0f;
BOOL shouldFinish;
if (ABS(velocity.y) > kTransitionGestureVelocityThreshold) {
shouldFinish = velocity.y > 0;
} else {
shouldFinish = location.y > kTransitionGestureLocationThreshold;
}
if (self.reversed) shouldFinish = !shouldFinish;
if (shouldFinish) {
[self.context finishInteractiveTransition];
} else {
[self.context cancelInteractiveTransition];
}
CGPoint finishCenter;
if (shouldFinish) {
if (self.reversed) {
finishCenter = CGPointMake(self.context.containerView.center.x, -100);
}else{
finishCenter = self.context.containerView.center;
}
}else{
if (self.reversed) {
finishCenter = self.context.containerView.center;
}else{
finishCenter = CGPointMake(self.context.containerView.center.x, -100);
}
}
if (shouldFinish) {
[self.context finishInteractiveTransition];
}else{
[self.context cancelInteractiveTransition];
}
self.finishingBehavior = [[FinishingBehavior alloc] initWithItem:self.view];
self.finishingBehavior.targetPoint = finishCenter;
if (!CGPointEqualToPoint(velocity, CGPointZero)) {
self.finishingBehavior.velocity = velocity;
}
__weak typeof(self) weakSelf = self;
self.finishingBehavior.action = ^{
if (!CGRectIntersectsRect(gesture.view.frame, weakSelf.view.frame)) {
[weakSelf.animator removeAllBehaviors];
}
};
[self.animator addBehavior:self.finishingBehavior];
break;
}
case UIGestureRecognizerStateCancelled:
break;
case UIGestureRecognizerStateFailed:
break;
default:
break;
}
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
[self.animator removeAllBehaviors];
for (UIGestureRecognizer *gesture in [[self.context containerView] gestureRecognizers]) {
[gesture.view removeGestureRecognizer:gesture];
}
[self.context completeTransition:![self.context transitionWasCancelled]];
}
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
self.context = transitionContext;
UIView *fromView = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView *toView = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey].view;
UIView *container = [transitionContext containerView];
if (!self.reversed) {
self.view = toView;
}else{
self.view = fromView;
}
NSTimeInterval duration = [self transitionDuration:transitionContext];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:container];
self.animator.delegate = self;
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPan:)];
[container addGestureRecognizer:gesture];
gesture.delegate = self;
self.startingTime = CACurrentMediaTime();
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
if (!self.reversed) {
self.view.bounds = CGRectMake(0, 0, 280, 180);
self.view.center = CGPointMake(container.center.x, -90);
[container addSubview:self.view];
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction animations:^{
self.view.center = container.center;
} completion:^(BOOL finished) {
[self.displayLink invalidate];
if (finished){
[gesture.view removeGestureRecognizer:gesture];
[transitionContext finishInteractiveTransition];
[transitionContext completeTransition:YES];
}
}];
}else{
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction animations:^{
self.view.center = CGPointMake(container.center.x, -90);
} completion:^(BOOL finished) {
[self.displayLink invalidate];
if (finished){
[gesture.view removeGestureRecognizer:gesture];
[transitionContext finishInteractiveTransition];
[transitionContext completeTransition:YES];
}
}];
}
}
@end
#pragma mark - DetailVC (Transition Delegate)
@interface DetailVC : UIViewController <UIViewControllerTransitioningDelegate>
@property (nonatomic, strong) CustomAnimatedTransition *transition;
@end
@implementation DetailVC
- (void)viewDidLoad
{
self.view.backgroundColor = [UIColor foregroundColor];
self.view.layer.cornerRadius = 10.0f;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
[self.view addGestureRecognizer:tapGesture];
}
- (void)tapped:(id)sender
{
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
self.transition = CustomAnimatedTransition.new;
return self.transition;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
self.transition.reversed = YES;
return self.transition;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)animator
{
return (id<UIViewControllerInteractiveTransitioning>)animator;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator
{
return (id<UIViewControllerInteractiveTransitioning>)animator;
}
@end
#pragma mark - RootVC
@interface RootVC : UIViewController
@end
@implementation RootVC
- (void)viewDidLoad
{
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
DetailVC *detailVC = [DetailVC new];
detailVC.modalPresentationStyle = UIModalPresentationCustom;
detailVC.transitioningDelegate = detailVC;
[self presentViewController:detailVC animated:YES completion:nil];
}
@end
#pragma mark - AppDelegate
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor backgroundColor];
self.window.rootViewController = [RootVC new];
[self.window makeKeyAndVisible];
return YES;
}
@end
@klefevre
Copy link

Thanks for the gist but it doesn't work as expected. The interactive part doesn't work like in your article http://initwithfunk.com/blog/2014/05/22/interactive-animated-transitions-on-ios/

Is it possible to have a patch ?

@dinarajas
Copy link

Maybe that's why they didn't post the project. 💨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment