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;

#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;
#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;
#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;
#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;
- (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;
- (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;
-(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;
#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;
-(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;
#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;
#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;
#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í;
Un comentario