UIAlertController se mueve a position de buggy en la parte superior de la pantalla cuando llama a `presentViewController:`

Presentar una vista desde un UIAlertController mueve la alerta a una position de buggy en la esquina superior izquierda de la pantalla. iOS 8.1, dispositivo y simulador.

Lo notamos en una aplicación cuando intentamos presentar una vista desde la vista actual "superior". Si un UIAlertController pasa a ser la vista superior, obtendremos este comportamiento. Hemos cambiado nuestro código para simplemente ignorar a UIAlertControllers, pero estoy publicando esto en caso de que otros golpeen el mismo problema (ya que no pude encontrar nada).

Hemos aislado esto en un simple proyecto de testing, el código completo al final de esta pregunta.

  1. Implementar viewDidAppear: en el View Controller en un nuevo proyecto Single View Xcode.
  2. Presente una alerta de UIAlertController .
  3. El controller de alerta llama inmediatamente a presentViewController:animated:completion: para mostrar y luego descartar otro controller de vista:

En el momento en que presentViewController:... el presentViewController:... comienza la animation, el UIAlertController se mueve a la esquina superior izquierda de la pantalla:

La alerta se ha movido a la parte superior de la pantalla.

Cuando la animation dismissViewControllerAnimated: termina, la alerta se ha movido aún más en el margen superior izquierdo de la pantalla:

La alerta se ha movido al margen superior izquierdo de la pantalla.

Código completo:

 - (void)viewDidAppear:(BOOL)animated { // Display a UIAlertController alert NSString *message = @"This UIAlertController will be moved to the top of the screen if it calls `presentViewController:`"; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"UIAlertController iOS 8.1" message:message prefernetworkingStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"I think that's a Bug" style:UIAlertActionStyleCancel handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; // The UIAlertController should Present and then Dismiss a view UIViewController *viewController = [[UIViewController alloc] init]; viewController.view.backgroundColor = self.view.tintColor; [alert presentViewController:viewController animated:YES completion:^{ dispatch_after(0, dispatch_get_main_queue(), ^{ [viewController dismissViewControllerAnimated:YES completion:nil]; }); }]; // RESULT: // UIAlertController has been moved to the top of the screen. // http://i.imgur.com/KtZobuK.png } 

¿Hay algo en el código anterior que estaría causando este problema? ¿Existen alternativas que permitan la presentación sin errores de una vista desde un UIAlertController?

rdar: // 19037589
http://openradar.appspot.com/19037589

rdar: // 19037589 fue cerrado por Apple

Relaciones con desarrolladores de Apple | 25 de febrero de 2015 10:52 a.m.

No hay planes para abordar esto en function de lo siguiente:

Esto no es compatible, evite presentarse en un UIAlertController.

Ahora estamos cerrando este informe.

Si tiene preguntas sobre la resolución, o si sigue siendo un problema crítico para usted, actualice su informe de errores con esa información.

Asegúrese de verificar regularmente las nuevas versiones de Apple para cualquier actualización que pueda afectar este problema.

Encontré una situación en la que a veces una vista modal se presentaba encima de una alerta (situación absurda, lo sé), y el UIAlertController podía aparecer en la parte superior izquierda (como la segunda captura de pantalla de la pregunta original ), y encontré un solución de una sola línea que parece funcionar. Para el controller que se presentará en UIAlertController, cambie su estilo de presentación modal de la siguiente manera:

 viewControllerToBePresented.modalPresentationStyle = .OverFullScreen 

Esto debería hacerse justo antes de llamar a presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)

Eso es un poco decepcionante … mover las alertas para ser UIViewControllers, pero luego rechazar algún uso regular de ellos. Yo trabajo en una aplicación que hizo algo similar: a veces necesita saltar a un nuevo context de usuario, y al hacerlo presentaba un nuevo controller de vista sobre cualquier cosa que estuviera allí. En realidad, tener las alertas como controlleres de vista es casi mejor en este caso, ya que se preservarían. Pero estamos viendo el mismo desplazamiento ahora que hemos cambiado a UIViewControllers.

Es posible que tengamos que encontrar una solución diferente (usando diferentes windows quizás), y tal vez evitamos presentar si el nivel superior es un UIAlertController. Pero, es posible restaurar el posicionamiento correcto. Puede que no sea una buena idea, porque el código podría romperse si Apple cambia su posicionamiento en la pantalla, pero la siguiente subclass parece funcionar (en iOS8) si esta funcionalidad es muy necesaria.

 @interface MyAlertController : UIAlertController @end @implementation MyAlertController /* * UIAlertControllers (of alert type, and action sheet type on iPhones/iPods) get placed in crazy * locations when you present a view controller over them. This attempts to restre their original placement. */ - (void)_my_fixupLayout { if (self.prefernetworkingStyle == UIAlertControllerStyleAlert && self.view.window) { CGRect myRect = self.view.bounds; CGRect windowRect = [self.view convertRect:myRect toView:nil]; if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero)) { CGPoint center = self.view.window.center; CGPoint myCenter = [self.view.superview convertPoint:center fromView:nil]; self.view.center = myCenter; } } else if (self.prefernetworkingStyle == UIAlertControllerStyleActionSheet && self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPhone && self.view.window) { CGRect myRect = self.view.bounds; CGRect windowRect = [self.view convertRect:myRect toView:nil]; if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero)) { UIScreen *screen = self.view.window.screen; CGFloat borderPadding = ((screen.nativeBounds.size.width / screen.nativeScale) - myRect.size.width) / 2.0f; CGRect myFrame = self.view.frame; CGRect superBounds = self.view.superview.bounds; myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2; myFrame.origin.y = superBounds.size.height - myFrame.size.height - borderPadding; self.view.frame = myFrame; } } } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self _my_fixupLayout]; } @end 

Apple puede considerar que el posicionamiento de vista sea privado, por lo que restaurarlo de esta manera puede no ser la mejor idea, pero funciona por ahora. Podría ser posible almacenar el marco antiguo en una anulación de -presentViewController: animated :, y simplemente restaurar eso en lugar de volver a calcular.

Es posible hacer que el propio UIAlertController haga lo equivalente a lo anterior, lo que también cubriría UIAlertControllers presentado por código que no controla, pero prefiero usar solo swizzles en lugares donde se trata de un error que Apple va a solucionar (por lo tanto, allí es un momento en que se puede eliminar el swizzle y permitimos que el código existente "simplemente funcione" sin eliminarlo solo por una solución de errores). Pero si es algo que Apple no va a solucionar (indicado por su respuesta como se indica en otra respuesta aquí), entonces generalmente es más seguro tener una class separada para modificar el comportamiento, por lo que lo está utilizando solo en circunstancias conocidas.

Creo que solo debes categorizar UIAlertController así:

 @implementation UIAlertController(UIAlertControllerExtended) - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (self.prefernetworkingStyle == UIAlertControllerStyleAlert) { __weak UIAlertController *pSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ CGRect screenRect = [[UIScreen mainScreen] bounds]; CGFloat screenWidth = screenRect.size.width; CGFloat screenHeight = screenRect.size.height; [pSelf.view setCenter:CGPointMake(screenWidth / 2.0, screenHeight / 2.0)]; [pSelf.view setNeedsDisplay]; }); } } @end 

También estaba teniendo este problema. Si presenté un controller de vista mientras se presentaba un UIAlertController, la alerta iría a la parte superior izquierda.

Mi solución es actualizar el centro de la vista del UIAlertController en viewDidLayoutSubviews; logrado subclassando UIAlertController.

 class MyBetterAlertController : UIAlertController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let screenBounds = UIScreen.mainScreen().bounds if (prefernetworkingStyle == .ActionSheet) { self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height - (self.view.frame.size.height*0.5) - 8) } else { self.view.center = CGPointMake(screenBounds.size.width*0.5, screenBounds.size.height*0.5) } } } 

Además de la respuesta de Carl Lindberg, hay dos casos que también deben tenerse en count:

  1. Dispositivo giratorio
  2. Altura del keyboard cuando hay un campo de text dentro de alerta

Entonces, la respuesta completa que funcionó para mí:

 // fix for rotation -(void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) { } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) { [self.view setNeedsLayout]; }]; } // fix for keyboard - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } -(void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)keyboardWillShow:(NSNotification *)notification { NSDictionary *keyboardUserInfo = [notification userInfo]; CGSize keyboardSize = [[keyboardUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; self.keyboardHeight = keyboardSize.height; [self.view setNeedsLayout]; } - (void)keyboardWillHide:(NSNotification *)notification { self.keyboardHeight = 0; [self.view setNeedsLayout]; } // position layout fix -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self fixAlertPosition]; } -(void)fixAlertPosition { if (self.prefernetworkingStyle == UIAlertControllerStyleAlert && self.view.window) { CGRect myRect = self.view.bounds; CGRect windowRect = [self.view convertRect:myRect toView:nil]; if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero)) { CGRect myFrame = self.view.frame; CGRect superBounds = self.view.superview.bounds; myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2; myFrame.origin.y = (superBounds.size.height - myFrame.size.height - self.keyboardHeight) / 2; self.view.frame = myFrame; } } else if (self.prefernetworkingStyle == UIAlertControllerStyleActionSheet && self.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPhone && self.view.window) { CGRect myRect = self.view.bounds; CGRect windowRect = [self.view convertRect:myRect toView:nil]; if (!CGRectContainsRect(self.view.window.bounds, windowRect) || CGPointEqualToPoint(windowRect.origin, CGPointZero)) { UIScreen *screen = self.view.window.screen; CGFloat borderPadding = ((screen.nativeBounds.size.width / screen.nativeScale) - myRect.size.width) / 2.0f; CGRect myFrame = self.view.frame; CGRect superBounds = self.view.superview.bounds; myFrame.origin.x = CGRectGetMidX(superBounds) - myFrame.size.width / 2; myFrame.origin.y = superBounds.size.height - myFrame.size.height - borderPadding; self.view.frame = myFrame; } } } 

Además, si usa la categoría, entonces necesita almacenar la altura del keyboard de alguna manera, así:

 @interface UIAlertController (Extended) @property (nonatomic) CGFloat keyboardHeight; @end @implementation UIAlertController (Extended) static char keyKeyboardHeight; - (void) setKeyboardHeight:(CGFloat)height { objc_setAssociatedObject (self,&keyKeyboardHeight,@(height),OBJC_ASSOCIATION_RETAIN); } -(CGFloat)keyboardHeight { NSNumber *value = (id)objc_getAssociatedObject(self, &keyKeyboardHeight); return value.floatValue; } @end 

El usuario manoj.agg publicó esta respuesta al informe de error Open Radar , pero dice:

De alguna manera, no tengo la reputación suficiente para publicar respuestas en Stackoverflow.

Publicando su respuesta aquí para la posteridad. No lo he probado / evaluado.


Paso 1:

Cree un controller de vista personalizado henetworkingado de UIViewController e implemente UIPopoverPresentationControllerDelegate :

 @interface CustomUIViewController : UIViewController<UITextFieldDelegate, UIPopoverPresentationControllerDelegate> 

Paso 2:

Presente la vista en pantalla completa, haciendo uso del popover de presentación:

 CustomUIViewController *viewController = [[CustomUIViewController alloc] init]; viewController.view.backgroundColor = self.view.tintColor; viewController.modalPresentationStyle = UIModalPresentationOverFullScreen; UIPopoverPresentationController *popController = viewController.popoverPresentationController; popController.delegate = viewController; [alert presentViewController:viewController animated:YES completion:^{ dispatch_after(0, dispatch_get_main_queue(), ^{ [viewController dismissViewControllerAnimated:YES completion:nil]; }); }]; 

Tuve un problema similar donde una vista de input de contraseña necesitaba ser mostrada encima de cualquier otro controller de vista, incluidos UIAlertControllers. El código anterior me ayudó a resolver el problema. Cambio notable en iOS 8 es que UIAlertController henetworkinga de UIViewController , que no fue el caso de UIAlertView .