¿Animar el cambio de los controladores de vista sin usar la pila del controlador de navegación, las subvistas o los controladores modales?

142

NavigationControllers tiene pilas de ViewController para administrar y transiciones de animación limitadas.

Agregar un controlador de vista como una vista secundaria a un controlador de vista existente requiere pasar eventos al controlador de vista secundaria, lo cual es difícil de manejar, cargado de pequeñas molestias y, en general, se siente como un mal truco cuando se implementa (Apple también recomienda no haciendo esto).

La presentación de un controlador de vista modal nuevamente coloca un controlador de vista encima de otro, y aunque no tiene el problema de pasar el evento descrito anteriormente, realmente no 'intercambia' el controlador de vista, lo apila.

Los guiones gráficos están limitados a iOS 5 y son casi ideales, pero no se pueden usar en todos los proyectos.

¿Puede alguien presentar un EJEMPLO DE CÓDIGO SÓLIDO para cambiar los controladores de vista sin las limitaciones anteriores y permitir transiciones animadas entre ellos?

Un ejemplo cercano, pero sin animación: cómo usar múltiples controladores de vista personalizados de iOS sin un controlador de navegación

Editar: el uso del controlador de navegación está bien, pero debe haber estilos de transición animados (no solo los efectos de diapositiva), el controlador de vista que se muestra debe intercambiarse por completo (no apilarse). Si el segundo controlador de vista debe eliminar otro controlador de vista de la pila, entonces no está lo suficientemente encapsulado.

Edición 2: iOS 4 debería ser el sistema operativo base para esta pregunta, debería haber aclarado eso al mencionar guiones gráficos (arriba).

TigreCoding
fuente
1
Puede hacer transiciones de animación personalizadas con un controlador de navegación. Si esto fuera aceptable, elimine esa restricción de su pregunta y publicaré un ejemplo de código.
Richard Brightwell
@ Richard si se salta la molestia de administrar la pila y acomoda diferentes estilos de transición animada entre los controladores de vista, ¡entonces el uso del controlador de navegación está bien!
TigerCoding
Bien. Me impaciente y publiqué el código. Darle una oportunidad. Funciona para mi.
Richard Brightwell el
@ RichardBrightwell, dijiste aquí que uno podría hacer transiciones de animación personalizadas entre los controladores de vista usando un controlador de navegación ... ¿cómo? Puede publicar un ejemplo? Gracias.
Pato

Respuestas:

108

EDITAR: Nueva respuesta que funciona en cualquier orientación. La respuesta original solo funciona cuando la interfaz está en orientación vertical. Estas son animaciones de transición de vista b / c que reemplazan una vista con una vista diferente que debe ocurrir con vistas al menos un nivel por debajo de la primera vista agregada a la ventana (por ejemplowindow.rootViewController.view.anotherView).

He implementado una clase de contenedor simple que llamé TransitionController. Puede encontrarlo en https://gist.github.com/1394947 .

Por otro lado, prefiero la implementación en una clase separada b / c, es más fácil de reutilizar. Si no quiere eso, simplemente podría implementar la misma lógica directamente en el delegado de su aplicación eliminando la necesidad de la TransitionControllerclase. Sin embargo, la lógica que necesitarías sería la misma.

Úselo de la siguiente manera:

En su delegado de aplicaciones

// add a property for the TransitionController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    MyViewController *vc = [[MyViewContoller alloc] init...];
    self.transitionController = [[TransitionController alloc] initWithViewController:vc];
    self.window.rootViewController = self.transitionController;
    [self.window makeKeyAndVisible];
    return YES;
}

Para pasar a un nuevo controlador de vista desde cualquier controlador de vista

- (IBAction)flipToView
{
    anotherViewController *vc = [[AnotherViewController alloc] init...];
    MyAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    [appDelegate.transitionController transitionToViewController:vc withOptions:UIViewAnimationOptionTransitionFlipFromRight];
}

EDITAR: Respuesta original a continuación: solo funciona para la orientación de retrato

Hice los siguientes supuestos para este ejemplo:

  1. Tiene un controlador de vista asignado como el rootViewControllerde su ventana

  2. Cuando cambia a una nueva vista, desea reemplazar el viewController actual con el viewController que posee la nueva vista. En cualquier momento, solo el viewController actual está activo (por ejemplo, asignado).

El código se puede modificar fácilmente para que funcione de manera diferente, el punto clave es la transición animada y el controlador de vista única. Asegúrese de no retener un controlador de vista en ningún lugar fuera de su asignación window.rootViewController.

Código para animar la transición en el delegado de la aplicación

- (void)transitionToViewController:(UIViewController *)viewController
                    withTransition:(UIViewAnimationOptions)transition
{
    [UIView transitionFromView:self.window.rootViewController.view
                        toView:viewController.view
                      duration:0.65f
                       options:transition
                    completion:^(BOOL finished){
                        self.window.rootViewController = viewController;
                    }];
}

Ejemplo de uso en un controlador de vista

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}
XJones
fuente
1
Si, muy lindo! No solo funciona, sino que es un ejemplo de código muy simple y limpio. ¡Muchas gracias!
TigerCoding
¿No le causa problemas en el paisaje? Además: ¿esto activa los métodos willAppear y didAppear de viewControllers?
Felix Lamouroux
1
Sé que las llamadas aparecen se están haciendo porque las registré. Tampoco veo por qué esto afectaría los cambios de orientación. ¿Puedes explicar por qué crees que lo haría?
TigerCoding
1
La gran solución @XJones, funciona bastante bien en mi aplicación de iPad, excepto por un problema que estoy enfrentando, después de la primera inicialización transVC = [TransitionController ... initWithViewController: aUINavCtrlObj]; window.rootVC = transVC; la vista en aUINavCtrlObj está acolchada 20px desde la parte superior (es decir, 20 px inferiores están fuera de los límites de la pantalla (UIWindow) en todas las orientaciones), pero después de hacer [transVC transitionToViewController: anotherVC] el relleno desaparece. Intenté wantsFullScreenLayout = NO en TransitionController loadView, lo que hace es agregar un área negra de 20 px justo debajo de statusBar.
Abduliam Rehmanius
1
@AbduliamRehmanius: tuve el mismo problema. Lo arreglé cambiando la línea 25 de TransitionController.ma UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];, pero solo lo he usado en la versión más reciente de iOS, así que pruébelo con cuidado.
Phil Calvin el
67

Puede usar el nuevo sistema de contención viewController de Apple. Para obtener información más detallada, consulte el video de la sesión de WWDC 2011 "Implementación de la UIViewControllercontención".

Nuevo en iOS5, UIViewControllerContainment le permite tener un viewController primario y varios viewControllers secundarios que están contenidos en él. Así es como funciona el UISplitViewController. Al hacer esto, puede apilar los controladores de vista en un padre, pero para su aplicación particular solo está usando el padre para administrar la transición de un viewController visible a otro. Esta es la forma aprobada por Apple de hacer las cosas y animar desde una vista infantil. ¡Además, puedes usar todas las diferentes UIViewAnimationOptiontransiciones!

Además, con UIViewContainment, no tiene que preocuparse, a menos que lo desee, por el desorden de administrar los controles de vista secundarios durante los eventos de orientación. Simplemente puede usar lo siguiente para asegurarse de que su parentViewController reenvíe los eventos de rotación a los viewControllers secundarios.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

Puede hacer lo siguiente o similar en el método viewDidLoad de sus padres para configurar el primer childViewController:

[self addChildViewController:self.currentViewController];
[self.view addSubview:self.currentViewController.view];
[self.currentViewController didMoveToParentViewController:self];
[self.currentViewController.swapViewControllerButton setTitle:@"Swap" forState:UIControlStateNormal];

luego, cuando necesite cambiar el viewController secundario, llame a algo similar a lo siguiente dentro del viewController primario:

-(void)swapViewControllers:(childViewController *)addChildViewController:aNewViewController{
     [self addChildViewController:aNewViewController];
     __weak __block ViewController *weakSelf=self;
     [self transitionFromViewController:self.currentViewController
                       toViewController:aNewViewController
                               duration:1.0
                                options:UIViewAnimationOptionTransitionCurlUp
                             animations:nil
                             completion:^(BOOL finished) {
                                   [aNewViewController didMoveToParentViewController:weakSelf];

                                   [weakSelf.currentViewController willMoveToParentViewController:nil];
                                   [weakSelf.currentViewController removeFromParentViewController];

                                   weakSelf.currentViewController=[aNewViewController autorelease];
                             }];
 }

Publiqué un proyecto de ejemplo completo aquí: https://github.com/toolmanGitHub/stackedViewControllers . Este otro proyecto muestra cómo usar la UIViewControllercontención en algunos tipos de controles de vista de entrada diferentes que no ocupan toda la pantalla. Buena suerte

timthetoolman
fuente
1
Un gran ejemplo Gracias por tomarse el tiempo de publicar el código. Por otro lado, se limita solo a iOS 5. Como se menciona en la pregunta: "[Storyboards] están limitados a iOS 5 y son casi ideales, pero no se pueden usar en todos los proyectos". Teniendo en cuenta que un gran porcentaje (¿alrededor del 40%?) De clientes todavía usan iOS 4, el objetivo es proporcionar algo que funcione en iOS 4 y versiones posteriores.
TigerCoding
¿No deberías llamar [self.currentViewController willMoveToParentViewController:nil];antes de la transición?
Felix Lamouroux
@FelixLam: según los documentos sobre la contención de UIViewController, llama a willMoveToParentViewController solo si anula el método addChildViewController. En este ejemplo, lo llamo pero no lo anulo.
timthetoolman
2
En la demostración en WWDC, parece recordar que lo llamaron antes de llamar a comenzar la transición, porque la transición no implica que el VC actual pasará a cero. En el caso de un UITabBarController, la transición no cambiaría ningún padre de vc. La eliminación de los padres llama a didMoveToParentViewController: nil, pero no hay will ... call. En mi humilde opinión
Felix Lamouroux
7

OK, sé que la pregunta dice sin usar un controlador de navegación, pero no hay razón para no hacerlo. OP no respondió a los comentarios a tiempo para que me fuera a dormir. No me rechaces. :)

Aquí le mostramos cómo hacer estallar el controlador de vista actual y pasar a un nuevo controlador de vista usando un controlador de navegación:

UINavigationController *myNavigationController = self.navigationController;
[[self retain] autorelease];

[myNavigationController popViewControllerAnimated:NO];

PreferencesViewController *controller = [[PreferencesViewController alloc] initWithNibName:nil bundle:nil];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.65];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:myNavigationController.view cache:YES];
[myNavigationController pushViewController:controller animated:NO];
[UIView commitAnimations];

[controller release];
Richard Brightwell
fuente
¿Esto no apila los controladores de vista?
TigerCoding
1
Sí, porque estamos usando un controlador de navegación. Sin embargo, evita la limitación sobre qué tipo de transiciones puede realizar, lo que pensé que era el núcleo de su pregunta.
Richard Brightwell el
Cerrar, pero uno de los grandes problemas es que hay múltiples controladores de vista para administrar en la pila. Estoy buscando la manera de cambiar completamente los controladores de vista. =)
TigerCoding
Ah También podría tener algo así ... dame un minuto. Si no, eliminaré esta respuesta.
Richard Brightwell el
Ok, improvisé dos partes diferentes de código. Creo que esto hará lo que quieras.
Richard Brightwell
3

Dado que acabo de encontrarme con este problema exacto y probé variaciones en todas las respuestas preexistentes a un éxito limitado, publicaré cómo finalmente lo resolví:

Como se describe en esta publicación sobre segues personalizados , en realidad es muy fácil hacer segues personalizados. También son súper fáciles de conectar en Interface Builder, mantienen visibles las relaciones en IB y no requieren mucho apoyo de los controladores de vista de origen / destino de la segue.

La publicación vinculada anteriormente proporciona el código iOS 4 para reemplazar el controlador de vista superior actual en la pila de NavigationController por uno nuevo que utiliza una animación de deslizamiento desde arriba.

En mi caso, quería que sucediera un reemplazo similar, pero con una FlipFromLefttransición. También solo necesitaba soporte para iOS 5+. Código:

Desde RAFlipReplaceSegue.h:

#import <UIKit/UIKit.h>

@interface RAFlipReplaceSegue : UIStoryboardSegue
@end

Desde RAFlipReplaceSegue.m:

#import "RAFlipReplaceSegue.h"

@implementation RAFlipReplaceSegue

-(void) perform
{
    UIViewController *destVC = self.destinationViewController;
    UIViewController *sourceVC = self.sourceViewController;
    [destVC viewWillAppear:YES];

    destVC.view.frame = sourceVC.view.frame;

    [UIView transitionFromView:sourceVC.view
                        toView:destVC.view
                      duration:0.7
                       options:UIViewAnimationOptionTransitionFlipFromLeft
                    completion:^(BOOL finished)
                    {
                        [destVC viewDidAppear:YES];

                        UINavigationController *nav = sourceVC.navigationController;
                        [nav popViewControllerAnimated:NO];
                        [nav pushViewController:destVC animated:NO];
                    }
     ];
}

@end

Ahora, arrastre el control para configurar cualquier otro tipo de segue, luego conviértalo en un Segue personalizado y escriba el nombre de la clase de segue personalizada, ¡y listo!

Ryan Artecona
fuente
¿Hay alguna manera de hacer esto programáticamente sin storyboard?
zakdances
Hasta donde yo sé, para usar un segue tienes que definirlo y darle un identificador en un guión gráfico. Puede invocar un segue en código con el –performSegueWithIdentifier:sender:método UIViewController .
Ryan Artecona
1
Nunca debe llamar a viewDidXXX y viewWillXXX directamente.
Ben Sinclair
2

Luché con este durante mucho tiempo, y uno de mis problemas se enumera aquí , no estoy seguro de si ha tenido ese problema. Pero esto es lo que recomendaría si debe funcionar con iOS 4.

En primer lugar, crea una nueva NavigationControllerclase. Aquí es donde haremos todo el trabajo sucio: otras clases podrán llamar "limpiamente" a métodos de instancia como pushViewController:y tal. En su .h:

@interface NavigationController : UIViewController {
    NSMutableArray *childViewControllers;
    UIViewController *currentViewController;
}

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion;
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeChildViewController:(UIViewController *)childController;

La matriz de controladores de vista secundarios servirá como una tienda para todos los controladores de vista en nuestra pila. Reenviaríamos automáticamente toda la rotación y el cambio de tamaño del código desde la NavigationControllervista de 's al currentController.

Ahora, en nuestra implementación:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
    currentViewController = [toViewController retain];
    // Put any auto- and manual-resizing handling code here

    [UIView animateWithDuration:duration animations:animations completion:completion];

    [fromViewController.view removeFromSuperview];
}

- (void)addChildViewController:(UIViewController *)childController {
    [childViewControllers addObject:childController];
}

- (void)removeChildViewController:(UIViewController *)childController {
    [childViewControllers removeObject:childController];
}

Ahora puede implementar su propia costumbre pushViewController:, popViewControllery tal, utilizando estas llamadas a métodos.

¡Buena suerte y espero que esto ayude!

aopsfan
fuente
Nuevamente, debemos recurrir a agregar controladores de vista como sub-vistas de los controladores de vista existentes. De acuerdo, esto es lo que hace el controlador de navegación existente, pero significa que básicamente tenemos que reescribirlo y todos sus métodos. En realidad, debemos evitar tener que distribuir viewWillAppear y métodos similares. Estoy empezando a pensar que no hay una manera limpia de hacer esto. Sin embargo, te agradezco por tomarte el tiempo y el esfuerzo.
TigerCoding
Creo que, con este sistema de agregar y quitar controladores de vista según sea necesario, esta solución evita que tenga que reenviar esos métodos.
aopsfan
¿Estás seguro? Antes solía intercambiar los controladores de vista de manera similar y siempre tenía que reenviar mensajes. ¿Puedes confirmar lo contrario?
TigerCoding
No, no estoy seguro, pero supongo que, siempre que elimine las vistas de los controladores de vista a medida que desaparecen, y las agregue a medida que aparezcan, eso debería activarse automáticamente viewWillAppear , viewDidAppeary tales.
aopsfan
-1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UINavigationController *viewController = (UINavigationController *)[storyboard instantiateViewControllerWithIdentifier:@"storyBoardIdentifier"];
viewController.modalTransitionStyle = UIModalTransitionStylePartialCurl;
[self presentViewController:viewController animated:YES completion:nil];

Prueba este código.


Este código proporciona la transición de un controlador de vista a otro controlador de vista que tiene un controlador de navegación.

Deepak Kumar Sahu
fuente