Yarneo
Yarneo

Reputation: 3000

Expanding circle animated transition between view controllers

I have created a custom animated transitioning using a transition delegate:

vc.modalTransitionStyle = UIModalPresentationCustom;
vc.transitioningDelegate = self.transitioningDelegate;

The transitioning delegate is working perfectly, but I am trying to achieve an effect that I have not succeeded in doing thus far. I am trying to make the new view controller appear like this effect:

Cool animated tranisitoning

I tried doing CGAffineTransformMakeScale, but it zooms the new view controller view, rather than just "expanding the circle of view into it". When trying to change with animations the bound and frame, it always fits the circle in the end with the screen bounds, and doesn't completely open up the circle like in the animation I gave.

Any ideas?

Thanks.

Upvotes: 2

Views: 3085

Answers (2)

rdelmar
rdelmar

Reputation: 104082

My first answer to this question was a modified version of an app I made a while ago, and would not be the best way to do it now (iOS 7 and beyond). In iOS 7 we can create custom transitions using the UIViewControllerTransitioningDelegate and associated delegates and classes. This version encapsulates all the mask creation and resizing in the animator object (the one that conforms to UIViewControllerAnimatedTransitioning), so it keeps the controller code simpler allowing you to use any controller as the destination controller with no special code. Here is the code I use in the class that's doing the presentation,

#import "ViewController.h"
#import "SecondViewController.h"
#import "RDPresentationAnimator.h"

@interface ViewController () <UIViewControllerTransitioningDelegate>
@property (weak,nonatomic) IBOutlet UIButton *button;
@end

@implementation ViewController

-(IBAction)presentSecondController:(UIButton *)sender {
    self.button = sender;
    SecondViewController *second = [self.storyboard instantiateViewControllerWithIdentifier:@"Second"];
    second.modalTransitionStyle = UIModalPresentationCustom;
    second.transitioningDelegate = self;
    [self presentViewController:second animated:YES completion:nil];
}

-(id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    RDPresentationAnimator *animator = [RDPresentationAnimator new];
    animator.isPresenting = YES;
    animator.senderFrame = self.button.frame;
    return animator;
}


-(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    NSLog(@"dismissed delegate called");
    RDPresentationAnimator *animator = [RDPresentationAnimator new];
    animator.isPresenting = NO;
    animator.senderFrame = self.button.frame;
    return animator;
}

Here's the code in the animator object. The .h,

@interface RDPresentationAnimator : NSObject <UIViewControllerAnimatedTransitioning>

@property (nonatomic) BOOL isPresenting;
@property (nonatomic) CGRect senderFrame;

And here's the .m,

@interface RDPresentationAnimator ()
@property (strong,nonatomic) UIBezierPath *maskPath;
@property (strong,nonatomic) UIViewController *toVC;
@property (strong,nonatomic) UIViewController *fromVC;
//@property (strong,nonatomic) id <UIViewControllerContextTransitioning> transitionContext;
@end

    @implementation RDPresentationAnimator

    #define ANIMATION_TIME 0.6



    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
        return ANIMATION_TIME;
    }


    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
       // self.transitionContext = transitionContext;
        self.fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
        self.toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

        if (self.isPresenting) {
            [transitionContext.containerView addSubview:self.fromVC.view];
            [transitionContext.containerView addSubview:self.toVC.view];

            self.maskPath = [UIBezierPath bezierPathWithOvalInRect:self.senderFrame];
            CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
            maskLayer.frame = self.toVC.view.frame;
            maskLayer.path = self.maskPath.CGPath;
            self.toVC.view.layer.mask = maskLayer;

            UIBezierPath *newPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-self.toVC.view.frame.size.width/2, -self.toVC.view.frame.size.height/2, self.toVC.view.frame.size.height*2, self.toVC.view.frame.size.height*2)];
            CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: @"path"];
            //pathAnim.delegate = self;
            pathAnim.fromValue = (id)self.maskPath.CGPath;
            pathAnim.toValue = (id)newPath.CGPath;
            pathAnim.duration = ANIMATION_TIME;
            maskLayer.path = newPath.CGPath;
            [maskLayer addAnimation:pathAnim forKey:@"path"];
        }else{
            [transitionContext.containerView addSubview:self.toVC.view];
            [transitionContext.containerView addSubview:self.fromVC.view];
            self.maskPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-self.fromVC.view.frame.size.width/2, -self.fromVC.view.frame.size.height/2, self.fromVC.view.frame.size.height*2, self.fromVC.view.frame.size.height*2)];
            CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
            maskLayer.frame = self.fromVC.view.frame;
            maskLayer.path = self.maskPath.CGPath;
            self.fromVC.view.layer.mask = maskLayer;

            UIBezierPath *newPath = [UIBezierPath bezierPathWithOvalInRect:self.senderFrame];
            CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: @"path"];
            //pathAnim.delegate = self;
            pathAnim.fromValue = (id)self.maskPath.CGPath;
            pathAnim.toValue = (id)newPath.CGPath;
            pathAnim.duration = ANIMATION_TIME;
            maskLayer.path = newPath.CGPath;
            [maskLayer addAnimation:pathAnim forKey:@"path"];
        }

        [self performSelector:@selector(finishTransition:) withObject:transitionContext afterDelay:ANIMATION_TIME];
    }



    -(void)finishTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
        [transitionContext completeTransition:YES];
    }


    //- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    //    NSLog(@"In didStop");
    //    if (flag) {
    //        if (self.isPresenting) {
    //            self.toVC.view.layer.mask = nil;
    //            [self.transitionContext completeTransition:YES];
    //        }else{
    //            self.fromVC.view.layer.mask = nil;
    //            [self.transitionContext completeTransition:YES];
    //            self.transitionContext = nil;
    //        }
    //    }
    //}


    -(void)dealloc {
        NSLog(@"In animator dealloc");
    }

This code also has the reverse animation for when you dismiss the modal controller (you only need to call dismissViewControllerAnimated:completion: like normal in that class).

Upvotes: 13

rdelmar
rdelmar

Reputation: 104082

I've made something like that in the past, but it doesn't have that expanding orange ring. My InitialViewController has a button added in the storyboard that has a round mask and a background image. The view controller I present has a mask whose size and position is set by passing in the frame of the button in InitialViewController that triggers the presentation. The presented view controller defines a protocol whose one method is invoked at the end of the animation. Here's the code in the initial view controller,

#import "InitialViewController.h"
#import "ViewController.h"

@interface InitialViewController () <ExpandingViewDelegate>
@end

@implementation InitialViewController


-(IBAction)expandToDetail:(UIButton *)sender { 
    ViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"VC"];
    vc.delegate = self;
    vc.buttonRect = sender.frame;
    [self.view addSubview:vc.view];
}


-(void)viewFinishedAnimating:(UIViewController *) sender { // delegate method from the presentedViewController
    [sender.view removeFromSuperview];
    [self presentViewController:sender animated:NO completion:nil];
}

Here's the code in the presented view controller, which performs the mask expansion. The .h file,

@protocol ExpandingViewDelegate <NSObject>
-(void)viewFinishedAnimating:(UIViewController *) sender;
@end

@interface ViewController : UIViewController

@property (nonatomic) CGRect buttonRect;
@property (weak,nonatomic) id<ExpandingViewDelegate> delegate;
@end

The .m file,

@interface ViewController ()
@property (strong,nonatomic) UIBezierPath *maskPath;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.maskPath = [UIBezierPath bezierPathWithOvalInRect:self.buttonRect];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.view.frame;
    maskLayer.path = self.maskPath.CGPath;
    self.view.layer.mask = maskLayer;
    [self enlargeMask:maskLayer];
}

-(void)enlargeMask:(CAShapeLayer *) shapeLayer {

    UIBezierPath *newPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(-self.view.frame.size.width/2, -self.view.frame.size.height/2, self.view.frame.size.height*2, self.view.frame.size.height*2)];
    CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: @"path"];
    pathAnim.delegate = self;
    pathAnim.fromValue = (id)self.maskPath.CGPath;
    pathAnim.toValue = (id)newPath.CGPath;
    pathAnim.duration = 6;
    shapeLayer.path = newPath.CGPath;
    [shapeLayer addAnimation:pathAnim forKey:@"path"];
}

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    if (flag) {
        self.view.layer.mask = nil;
        [self.delegate viewFinishedAnimating:self];
    }
}

Upvotes: 3

Related Questions