iphone - human - ¿La mejor forma de cambiar entre UISplitViewController y otros controladores de vista?
submit app to app store (9)
Estoy creando una aplicación para iPad. Una de las pantallas de la aplicación es perfectamente adecuada para usar un UISplitViewController. Sin embargo, el nivel superior de la aplicación es un menú principal, para el cual no quiero usar UISplitViewController. Esto presenta un problema, porque Apple dice que:
UISplitViewController
debe ser el controlador de vista de nivel superior en la aplicación, es decir, su vista debe agregarse como la subvista deUIWindow
si se utiliza,
UISplitViewController
debe estar allí durante toda la vida de la aplicación, es decir, no eliminar su vista de UIWindow y poner otra en su lugar, o viceversa
Después de haber leído y experimentado, parece que la única opción viable es satisfacer los requisitos de Apple y la nuestra es usar diálogos modales. Entonces, nuestra aplicación tiene un UISplitViewController en el nivel raíz (es decir, su vista se agrega como la subvista de UIWindow), y para mostrar nuestro menú principal, lo presionamos como un cuadro de diálogo modal de pantalla completa en el UISplitViewController. Luego, descartando el diálogo modal del controlador de vista de menú principal, podemos mostrar nuestra vista dividida.
Esta estrategia parece funcionar bien. Pero plantea las preguntas:
1) ¿Hay alguna forma mejor de estructurar esto, sin modales, que también cumpla con todos los requisitos mencionados? Parece un poco extraño tener la interfaz de usuario principal aparece en virtud de ser empujado como un diálogo modal. (Los modos se supone que son para tareas de usuario enfocadas).
2) ¿Estoy en riesgo de rechazo de la tienda de aplicaciones debido a mi enfoque? Esta estrategia modal es probablemente ''mal uso'' de diálogos modales, según las pautas de interfaz humana de Apple. Pero, ¿qué otra opción me han dado? ¿Sabrían ellos que estoy haciendo esto, de todos modos?
¿Y quién dijo que solo puedes tener una ventana? :)
Vea si mi respuesta sobre esta pregunta similar puede ayudar.
Este enfoque está funcionando muy bien para mí. Siempre que no tenga que preocuparse por las pantallas múltiples o la restauración del estado, este código vinculado debería ser suficiente para hacer lo que necesita: no tiene que hacer que su lógica mire hacia atrás ni reescribir el código existente, y aún así puede aprovecharlo de UISplitView en un nivel más profundo dentro de su aplicación, sin (AFAIK) rompiendo las pautas de Apple.
Acabo de toparme con este problema en un proyecto y pensé que compartiría mi solución. En nuestro caso (para iPad), queríamos comenzar con un UISplitViewController
con ambos controladores de vista visibles (usando preferredDisplayMode = .allVisible
). En algún punto de la jerarquía de detalles (derecha) (también teníamos un controlador de navegación para este lado), queríamos empujar un nuevo controlador de vista sobre todo el controlador de vista dividida (no usar una transición modal).
En iPhone, este comportamiento es gratis, ya que solo un controlador de vista está visible en cualquier momento. Pero en el iPad tuvimos que pensar en algo más. Terminamos yendo con un controlador de vista de contenedor raíz que agrega el controlador de vista dividida como un controlador de vista secundaria. Este controlador de vista raíz está incrustado en un controlador de navegación. Cuando el controlador de vista detallada en el controlador de vista dividida quiere empujar un nuevo controlador sobre todo el controlador de vista dividida, el controlador de vista raíz empuja este nuevo controlador de vista con su controlador de navegación.
Agregando a la respuesta de @tadija estoy en una situación similar:
Mi aplicación era solo para teléfonos, y estoy agregando una tableta UI. Decidí hacerlo en Swift en la misma aplicación y eventualmente migrar toda la aplicación para usar el mismo guión gráfico (cuando siento que la versión de iPad es estable, usarla para teléfonos debería ser trivial con las nuevas clases de XCode6).
Aún no se definieron segues en mi escena y todavía funciona.
Mi código en el delegado de mi aplicación está en ObjectiveC, y es ligeramente diferente, pero usa la misma idea. Tenga en cuenta que estoy usando el controlador de vista predeterminado de la escena, a diferencia de los ejemplos anteriores. Creo que esto también funcionará en IOS7 / IPhone en el que el tiempo de ejecución generará un UINavigationController
regular en lugar de un UISplitViewController
. Incluso podría agregar un nuevo código que presione el controlador de vista de inicio de sesión en IPhones, en lugar de cambiar el rootVC.
- (void) setupRootViewController:(BOOL) animated {
UIViewController *newController = nil;
UIStoryboard *board = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
UIViewAnimationOptions transition = UIViewAnimationOptionTransitionCrossDissolve;
if (!loggedIn) {
newController = [board instantiateViewControllerWithIdentifier:@"LoginViewController"];
} else {
newController = [board instantiateInitialViewController];
}
if (animated) {
[UIView transitionWithView: self.window duration:0.5 options:transition animations:^{
self.window.rootViewController = newController;
NSLog(@"setup root view controller animated");
} completion:^(BOOL finished) {
NSLog(@"setup root view controller finished");
}];
} else {
self.window.rootViewController = newController;
}
}
Hice una UISplitView como vista inicial, que va modalmente a una UIView de pantalla completa y de regreso a UISplitView. Si necesita regresar a SplitView, debe usar un segue personalizado.
Lea este enlace (tradúzcalo desde japonés)
Me gustaría contribuir con mi enfoque para presentar un UISplitViewController, como te gustaría a través de -presentViewController:animated:completion:
(todos sabemos que no funcionará). Creé una subclase UISplitViewController que responde a:
-presentAsRootViewController
-returnToPreviousViewController
La clase, que al igual que otros enfoques exitosos, establece el UISplitViewController como el control raíz de la ventana, pero lo hace con una animación similar a la que obtienes (de forma predeterminada) con -presentViewController:animated:completion:
PresentableSplitViewController.h
#import <UIKit/UIKit.h>
@interface PresentableSplitViewController : UISplitViewController
- (void) presentAsRootViewController;
@end
PresentableSplitViewController.m
#import "PresentableSplitViewController.h"
@interface PresentableSplitViewController ()
@property (nonatomic, strong) UIViewController *previousViewController;
@end
@implementation PresentableSplitViewController
- (void) presentAsRootViewController {
UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
_previousViewController=window.rootViewController;
UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
window.rootViewController = self;
[window insertSubview:windowSnapShot atIndex:0];
CGRect dstFrame=self.view.frame;
CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
offset.width*=self.view.frame.size.width;
offset.height*=self.view.frame.size.height;
self.view.frame=CGRectOffset(self.view.frame, offset.width, offset.height);
[UIView animateWithDuration:0.5
delay:0.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
self.view.frame=dstFrame;
} completion:^(BOOL finished) {
[windowSnapShot removeFromSuperview];
}];
}
- (void) returnToPreviousViewController {
if(_previousViewController) {
UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
window.rootViewController = _previousViewController;
[window addSubview:windowSnapShot];
CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
offset.width*=windowSnapShot.frame.size.width;
offset.height*=windowSnapShot.frame.size.height;
CGRect dstFrame=CGRectOffset(windowSnapShot.frame, offset.width, offset.height);
[UIView animateWithDuration:0.5
delay:0.0
usingSpringWithDamping:1.0
initialSpringVelocity:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
windowSnapShot.frame=dstFrame;
} completion:^(BOOL finished) {
[windowSnapShot removeFromSuperview];
_previousViewController=nil;
}];
}
}
@end
Otra opción: en el controlador de vista de detalles, muestro un controlador de vista modal:
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if (!appDelegate.loggedIn) {
// display the login form
let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
let login = storyboard.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
self.presentViewController(login, animated: false, completion: { () -> Void in
// user logged in and is valid now
self.updateDisplay()
})
} else {
updateDisplay()
}
No descarte el controlador de inicio de sesión sin configurar el indicador de inicio de sesión. Tenga en cuenta que en IPhones, el controlador de vista maestra será el primero, por lo que un código muy similar deberá estar en el controlador de vista maestra.
Para futuros desarrolladores de iOS que tengan el mismo problema: aquí hay otra respuesta y explicaciones. TIENES que convertirlo en controlador de vista raíz. Si no es así, superponga un modal.
UISplitviewcontroller no como un controlador de vistas de root
Touche! Se encontró con el mismo problema y lo resolvió de la misma manera usando modales. En mi caso, era una vista de inicio de sesión y luego el menú principal también para mostrarse antes de la vista dividida. Usé la misma estrategia que pensaste. Yo (así como otros mucha gente inteligente de iOS con la que hablé) no pude encontrar una mejor salida. Funciona bien para mí El usuario nunca nota el modal de todos modos. Preséntalos así. Y sí, también puedo decirte que hay bastantes aplicaciones haciendo lo mismo bajo los trucos de la tienda de aplicaciones. :) En otra nota, hágamelo saber si usted encuentra una mejor manera de salir de alguna manera alguna vez :)
En serio, no creía que este concepto de tener algún UIViewController para mostrar antes de UISplitViewController (formulario de inicio de sesión, por ejemplo) resulta ser tan complicado, hasta que tuve que crear ese tipo de vista jerárquica.
Mi ejemplo se basa en iOS 8 y XCode 6.0 (Swift), por lo que no estoy seguro de si este problema existía anteriormente de la misma manera, o se debe a algunos nuevos errores introducidos con iOS 8, pero a partir de todas las preguntas similares, encontrado, no vi solución completa ''no muy hacky'' a este problema.
Te guiaré por algunas de las cosas que probé antes de terminar con una solución (al final de esta publicación). Cada ejemplo se basa en la creación de un nuevo proyecto a partir de la plantilla Master-Detail sin CoreData habilitado.
Primer intento (transición modal a UISplitViewController):
- crear una nueva subclase UIViewController (LoginViewController por ejemplo)
- agregue un nuevo controlador de vista en el guión gráfico, configúrelo como controlador de vista inicial (en lugar de UISplitViewController) y conéctelo a LoginViewController
- agregue UIButton a LoginViewController y cree una transición modal de ese botón a UISplitViewController
- mover el código de configuración repetitivo para UISplitViewController desde el
didFinishLaunchingWithOptions
didFinishLaunchingWithOptions de AppDelegate aldidFinishLaunchingWithOptions
de LoginViewController
Esto casi funcionó. Digo casi, porque después de que la aplicación se inicia con LoginViewController y toca el botón y pasa a UISplitViewController, hay un extraño error: mostrar y ocultar el controlador de vista maestra en el cambio de orientación ya no está animado.
Después de un tiempo luchando con este problema y sin una solución real, pensé que de alguna manera estaba conectado con esa extraña regla de que UISplitViewController debe ser rootViewController (y en este caso no lo es, LoginViewController) así que renuncié a esta solución no tan perfecta .
Segundo intento (transición modal desde UISplitViewController):
- crear una nueva subclase UIViewController (LoginViewController por ejemplo)
- agregar nuevo controlador de vista en el guión gráfico y conectarlo a LoginViewController (pero esta vez deje que UISplitViewController sea el controlador de vista inicial)
- crear transición modal de UISplitViewController a LoginViewController
- agregue UIButton a LoginViewController y cree un desenrollado segue desde ese botón
Finalmente, agregue este código a didFinishLaunchingWithOptions
de AppDelegate después del código repetitivo para configurar UISplitViewController:
window?.makeKeyAndVisible()
splitViewController.performSegueWithIdentifier("segueToLogin", sender: self)
return true
o intente con este código en su lugar:
window?.makeKeyAndVisible()
let loginViewController = splitViewController.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
splitViewController.presentViewController(loginViewController, animated: false, completion: nil)
return true
Ambos ejemplos producen las mismas cosas malas:
- Salidas de consola:
Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
- UISplitViewController debe mostrarse primero antes de iniciar sesión de modo automático (prefiero presentar únicamente el formulario de inicio de sesión para que el usuario no vea UISplitViewController antes de iniciar sesión)
- Desactivar segue no se llama (esto es totalmente otro error, y no voy a entrar en esa historia ahora)
Solución (actualizar rootViewController)
La única forma que encontré que funciona correctamente es si cambia el control de raíz de la ventana sobre la marcha:
- Defina Storyboard ID para LoginViewController y UISplitViewController, y agregue algún tipo de propiedad loggedIn a AppDelegate.
- En función de esta propiedad, crea una instancia del controlador de vista apropiado y luego configúralo como rootViewController.
- Hazlo sin animación en
didFinishLaunchingWithOptions
pero animado cuando se llama desde la interfaz de usuario.
Aquí hay un código de muestra de AppDelegate:
var loggedIn = false
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
setupRootViewController(false)
return true
}
func setupRootViewController(animated: Bool) {
if let window = self.window {
var newRootViewController: UIViewController? = nil
var transition: UIViewAnimationOptions
// create and setup appropriate rootViewController
if !loggedIn {
let loginViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
newRootViewController = loginViewController
transition = .TransitionFlipFromLeft
} else {
let splitViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
let controller = masterNavigationController.topViewController as MasterViewController
newRootViewController = splitViewController
transition = .TransitionFlipFromRight
}
// update app''s rootViewController
if let rootVC = newRootViewController {
if animated {
UIView.transitionWithView(window, duration: 0.5, options: transition, animations: { () -> Void in
window.rootViewController = rootVC
}, completion: nil)
} else {
window.rootViewController = rootVC
}
}
}
}
Y este es código de muestra de LoginViewController:
@IBAction func login(sender: UIButton) {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
delegate.loggedIn = true
delegate.setupRootViewController(true)
}
También me gustaría saber si hay alguna forma mejor / más limpia para que esto funcione correctamente en iOS 8.