Transiciones personalizadas en iOS7
Índice de contenidos
1. Introducción
2. Entorno
3. Creando proyecto.
4. Conclusiones.
1. Introducción
IOS7 introduce nuevas clases para facilitar a los desarrolladores la posibilidad de hacer transiciones a nuestro gusto, tanto en la presentación de pantallas de forma modal como en los UINavigationsControllers y los UITabBarControllers, por lo que el límite es nuestra imaginación, empezamos a ver un poco de teoría, las clases que juegan en este papel son las siguientes;
- UIViewControllerAnimatedTransitioning: Este protocolo tiene tres métodos, uno de ellos opcional, los principales son (transitionDuration: ) y (animateTransition: ) en los cuales se determina la duración y la animación que se va a ejecutar.
- UIViewControllerTransitioningDelegate: Este es el protocolo al que tiene que acogerse el controlador que implemente las transiciones modales personalizadas, tiene 4 métodos opcionales, dos para las transiciones por defecto (animationControllerForPresentedController: presentingController:sourceController:) y (animationControllerForDismissedController:) para presentar y ocultar respectivamente y otros dos para las transiciones interactivas, en las cuales se puede interactuar con la animación con un gesto por ejemplo (interactionControllerForPresentation:) y (interactionControllerForDismissal:).
- UIViewControllerContextTransitioning: Este protocolo nos proporcionara toda la información acerca del contexto de la transición, no tiene implementación , solo nos provee de información.
- UINavigationControllerDelegate: Este ya le conoceréis unos cuantos, pero ahora en iOS7 tiene un par de métodos más para la implementación de las transiciones personalizadas, el primero es (navigationController:interactionControllerForAnimationController:) para las transiciones interactivas y (navigationController: animationControllerForOperation:fromViewController:toViewController:) para las transiciones por defecto.
- UITabBarControllerDelegate: Al igual que el protocolo del navigationController en iOS7 añade unos metodos para las transiciones (tabBarController:interactionControllerForAnimationController:) para las transiciones interactivas y (tabBarController:animationControllerForTransitionFromViewController:toViewController:) para las de por defecto.
Bien una vez vistos los protocolos y métodos vamos a ver un poco de código, vamos a crear un proyecto que haga dos tipos de transiciones, una de tipo modal y otra de tipo de navegación.
2. Entorno
- Macbook pro core i7 con 8gb RAM
- SO: Mavericks
- IDE: Xcode 5.0.2.
3. Creando proyecto.
Vamos a crear un nuevo proyecto en Xcode de tipo single View aplication, y vamos a borrar el controlador del storyboard que viene por defecto y añadir unUINavigarionController, recordar poner un identifirer a la celda que viene por defecto en el UITableViewController. Vamos a crear una clase que herede UITableViewController llamada MasterViewController y la vamos a linkar al controlador de tipo tabla que nos ha arrastrado el UINavigationController.
Bien ahora vamos a crear un fichero .h al que llamaremos AnimationDelegate.h y escribimos lo siguiente;
1 2 3 4 5 6 7 8 9 10 11 |
#import @protocol IACAnimationControllerDelegate <UIViewControllerAnimatedTransitioning> @property (nonatomic, assign) NSTimeInterval presentationDuration; @property (nonatomic, assign) NSTimeInterval dismissalDuration; @property (nonatomic, assign) BOOL isPresenting; @end |
Ahora creamos una clase que herede NSObject a la que llamaremos ModalTransition e importamos el fichero que acabamos de crear y adoptamos su protocolo, también vamos a crear las propertys necesarias y los métodos del protocolo UIViewControllerAnimatedTransitioning, nos quedaría de esta forma;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#import #import "IACAnimationController.h" @interface IACModalTransition : NSObject<IACAnimationControllerDelegate> @property (nonatomic ) BOOL isPresenting; @property (nonatomic, assign) NSTimeInterval presentationDuration; @property (nonatomic, assign) NSTimeInterval dismissalDuration; @property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext; -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext; -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext; @end |
Bien, ahora vamos a implementar los métodos en el .m;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#import "IACModalTransition.h" @implementation IACModalTransition -(id)init{ self = [super init]; if(self){ self.presentationDuration = 1.0; self.dismissalDuration = 0.5; } return self; } -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{ return self.isPresenting ? self.presentationDuration : self.dismissalDuration; } -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ } @end |
Antes de ponernos con la magia de CoreAnimation vamos a preparar la tabla, en la clase controladora que hemos llamado MasterViewController, vamos al .h y vamos a crear un array para poblar la tabla, una propiedad para nuestro AnimationController y nos vamos a acoger a los protocolos que vamos a necesitar para hacer nuestras transiciones, nos quedaría de esta forma;
1 2 3 4 5 6 7 8 9 |
#import #import "IACAnimationController.h" @interface IACMasterViewController : UITableViewController<UINavigationControllerDelegate,UIViewControllerTransitioningDelegate> @property(nonatomic,strong) NSArray *transitionsTypes; @property(nonatomic, strong) id<IACAnimationControllerDelegate>animationController; @end |
Bien en el .m vamos a iniciar el array y decirle a la tabla que tiene 1 sección y dos celdas y poblamos las celdas con el array, también vamos implementar el método didSelectRowAtIndexPath: para la acción de pulsar la celda y decirle al controlador quién es el delegado del UINavigationController, nos quedaría de esta forma;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
- (void)viewDidLoad { [super viewDidLoad]; self.navigationController.delegate = self; self.transitionsTypes = @[@"Transición modal",@"Transición de navegación"]; } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return 2; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; cell.textLabel.text = [self.transitionsTypes objectAtIndex: indexPath.row]; return cell; } -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ } |
Bien, ahora vamos a crear otro controlador en el storyboard que sea un UIViewController y colocamos una imagen de fondo, y un botón en la parte inferior.
Vamos a crear su clase y la Llamaremos ImageViewController, después vamos a crear los Outlets para crear la acción del botón y la propiedad de la imageView, también vamos a indicarle al storyboard el ID de nuestro controlador;
Vamos a indicar en el IBAction del botón que queremos hacer un DissMiss;
1 2 3 |
- (IBAction)actionBack:(id)sender { [self dismissViewControllerAnimated:YES completion:NULL]; } |
Ahora vamos a importar nuestro nuevo controlador y la clase ModalTransition en la clase MasterViewController y en el método de selección de la celda haremos lo siguiente;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ IACImageViewController *detailVC = [self.storyboard instantiateViewControllerWithIdentifier:@"IACImageViewController"]; detailVC.transitioningDelegate = self; switch (indexPath.row) { case 0: self.animationController = [IACModalTransition new]; [self presentViewController:detailVC animated:YES completion:nil]; break; } } |
Ahora vamos a implementar en este mismo controlador los métodos para las transiciones modales;
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#pragma mark UIViewControllerAnimatedTransitioning -(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ self.animationController.isPresenting = YES; return self.animationController; } -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ self.animationController.isPresenting = NO; return self.animationController; } |
Y ya por fin volvemos a la clase ModalTransition para hacer la animación, voy a comentar el código para que quede claro lo que vamos a ir haciendo;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ self.transitionContext = transitionContext; if(self.isPresenting){ [self animatePresenting:transitionContext]; } else{ [self animateDissMiss:transitionContext]; } } -(void)animatePresenting:(id<UIViewControllerContextTransitioning>)transitionContext { //Obtenemos los controladores del contexto UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; //obtenemos la camptura del controlador del que venimos para animar una captura en vez de una vista UIView *capture1 = [[fromViewController.view snapshotViewAfterScreenUpdates:YES] resizableSnapshotViewFromRect:CGRectMake(0, 0, 160, 640) afterScreenUpdates:YES withCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; UIView*capture2 = [[fromViewController.view snapshotViewAfterScreenUpdates:YES] resizableSnapshotViewFromRect:CGRectMake(160, 0, 160, 640) afterScreenUpdates:YES withCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; capture2.center = CGPointMake(capture1.center.x *3, capture1.center.y); //creamos la vista contenedora de la animación y borro la vista del controlador del que venimos UIView *containerView = [transitionContext containerView]; [fromViewController.view removeFromSuperview]; //añando la vista [containerView addSubview:toViewController.view]; //preparo la captura [containerView addSubview:capture1]; [containerView addSubview:capture2]; //seteo el estado inicial del controlador al que vamos CATransform3D scale = CATransform3DIdentity; toViewController.view.layer.transform = CATransform3DScale(scale, 0.0, 0.0, 1); toViewController.view.alpha = 0.6; // obtengo las transformaciones CATransform3D t1 = [self firstTransform]; CATransform3D t2 = [self secondTransform]; [UIView animateKeyframesWithDuration:self.presentationDuration delay:0.0 options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{ [UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.5f animations:^{ //animación del fromVieController dividiendose en dos capture1.layer.transform = t1; capture1.center = CGPointMake(capture1.center.x - 120, capture1.center.y ); capture2.layer.transform = t2; capture2.center = CGPointMake(capture2.center.x + 120, capture1.center.y ); }]; [UIView addKeyframeWithRelativeStartTime:0.5f relativeDuration:0.5f animations:^{ //animación del toVieController acercandose toViewController.view.layer.transform = CATransform3DIdentity; toViewController.view.alpha = 1.0f; }]; } completion:^(BOOL finished) { [self.transitionContext completeTransition:YES]; }]; } -(void)animateDissMiss:(id<UIViewControllerContextTransitioning>)transitionContext { //Obtenemos los controladores del contexto UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; //obtenemos la captura del controlador al que vamos, la dividimos en dos Y LA DEJAMOS YA ROTADA. UIView *capture1 = [[toViewController.view snapshotViewAfterScreenUpdates:YES] resizableSnapshotViewFromRect:CGRectMake(0, 0, 160, 640) afterScreenUpdates:YES withCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; capture1.layer.transform = [self firstTransform]; capture1.center = CGPointMake(capture1.center.x - 120, capture1.center.y); UIView*capture2 = [[toViewController.view snapshotViewAfterScreenUpdates:YES] resizableSnapshotViewFromRect:CGRectMake(160, 0, 160, 640) afterScreenUpdates:YES withCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; capture2.center = CGPointMake(capture2.center.x *3 + 120, capture2.center.y); capture2.layer.transform = [self secondTransform]; // obtengo la captura del controlador del que venimos UIView *intermediateView = [fromViewController.view snapshotViewAfterScreenUpdates:YES]; //creamos la vista contenedora de la animación y borro la vista del controlador del que venimos UIView *containerView = [transitionContext containerView]; [fromViewController.view removeFromSuperview]; //añando la vista las campuras [containerView addSubview:intermediateView]; [containerView addSubview:capture1]; [containerView addSubview:capture2]; CATransform3D scale = CATransform3DIdentity; [UIView animateKeyframesWithDuration:self.presentationDuration delay:0.0 options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{ [UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.5f animations:^{ //animación del fromViewController alejandose intermediateView.layer.transform = CATransform3DScale(scale, 0.0, 0.0, 1); intermediateView.alpha = 0.6; }]; [UIView addKeyframeWithRelativeStartTime:0.5f relativeDuration:0.5f animations:^{ //animación del toVieController uniendose de las dos partes capture1.layer.transform = CATransform3DIdentity; capture1.center = CGPointMake(capture1.center.x + 120, capture1.center.y ); capture2.layer.transform = CATransform3DIdentity; capture2.center = CGPointMake(capture2.center.x - 120, capture1.center.y); }]; } completion:^(BOOL finished) { [self.transitionContext completeTransition:YES]; }]; } -(CATransform3D)firstTransform{ CATransform3D transform = CATransform3DIdentity; transform.m34 = 1.0/-900; transform = CATransform3DRotate(transform, 90.0f, 0, 1, 0); return transform; } -(CATransform3D)secondTransform{ CATransform3D transform = CATransform3DIdentity; transform.m34 = 1.0/-900; transform = CATransform3DRotate(transform, -90.0f, 0, 1, 0); return transform; } |
Si compilamos ahora veréis que hace una animación que divide en dos el controlador actual y acerca el siguiente.
Ahora vamos a hacer una transición para el UINavigationController, vamos a crear una clase que herede NSObject llamada NavigationTransition y vamos a dejar nuestro .h de la misma forma que el ModalTransition;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#import <Foundation/Foundation.h> #import "IACAnimationController.h" @interface IACNavigationTransition : NSObject<IACAnimationControllerDelegate> @property (nonatomic ) BOOL isPresenting; @property (nonatomic, assign) NSTimeInterval presentationDuration; @property (nonatomic, assign) NSTimeInterval dismissalDuration; @property (nonatomic, strong) id<UIViewControllerContextTransitioning> transitionContext; -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext; -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext; @end |
El .m vamos a dejarlo de esta forma;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
#import "IACNavigationTransition.h" @implementation IACNavigationTransition -(id)init{ if (self = [super init]) { self.presentationDuration = 1.0; self.dismissalDuration = 1.0; } return self; } -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext { //determinamos el tiempo que durará la animación return self.isPresenting ? self.presentationDuration:self.dismissalDuration; } -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext { self.transitionContext = transitionContext; if (self.isPresenting) { [self animatePresenting:self.transitionContext]; }else{ [self animateDissMiss:self.transitionContext]; } } -(void)animatePresenting:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *container = [transitionContext containerView]; fromViewController.view.frame = container.frame; toViewController.view.frame = container.frame; // coloco el toViewController en la aprte superior y boca abajo toViewController.view.transform = CGAffineTransformMakeRotation(-M_PI); toViewController.view.layer.anchorPoint = CGPointMake(0.5, 0.0); toViewController.view.layer.position = CGPointMake(160.0, 0); //inserto los controladores en el contenedor [container insertSubview:toViewController.view belowSubview:fromViewController.view]; [UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:.8 initialSpringVelocity:6.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ toViewController.view.layer.transform = CATransform3DIdentity; fromViewController.view.center = CGPointMake(-container.frame.size.width, container.frame.size.height/2); fromViewController.view.transform = CGAffineTransformMakeRotation(M_PI/2); } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } -(void)animateDissMiss:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; CGRect initialRect = [transitionContext initialFrameForViewController:fromViewController]; //coloco el formViewController en el centro y le indico su punto de anclaje y la posición desde la que rota CGAffineTransform rotation; rotation = CGAffineTransformMakeRotation(M_PI); fromViewController.view.frame = initialRect; fromViewController.view.layer.anchorPoint = CGPointMake(0.5, 0.0); fromViewController.view.layer.position = CGPointMake(160.0, 0); //inserto los controladores en el contenedor UIView *container = [transitionContext containerView]; [container insertSubview:toViewController.view belowSubview:fromViewController.view]; //coloco el toViewController del revés y en la parte izquierda toViewController.view.center = CGPointMake(-initialRect.size.width, initialRect.size.height); toViewController.view.transform = rotation; [UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:.8 initialSpringVelocity:6.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ //rotamos el controlador del que venimos hacia arriba y centramos al que vamos fromViewController.view.transform = rotation; toViewController.view.frame = initialRect; toViewController.view.transform = CGAffineTransformMakeRotation(0); } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } |
Esta animación rota las vistas con un efecto de rebote muy chulo ;-).
Ahora vamos a implementar los métodos para que nuestro UINavigationController ejecute nuestra transición;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#pragma mark - navigationController Delegate -(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ switch (operation) { case UINavigationControllerOperationPush: self.animationController.isPresenting = YES; return self.animationController; case UINavigationControllerOperationPop: self.animationController.isPresenting = NO; return self.animationController; default: return nil; } } |
4. Conclusiones.
Pues como veis se pueden hacer cosas muy chulas con las transiciones personalizadas, podéis bajaros el proyecto aquí;
[…] Novedades desde IOS 7 (Animación de transiciones) […]