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í;
[…] Novedades desde IOS 7 (Animación de transiciones) […]