presentViewController: animado: la vista SÍ no aparecerá hasta que el usuario vuelva a tocar

93

Estoy teniendo un comportamiento extraño con presentViewController:animated:completion. Lo que estoy haciendo es esencialmente un juego de adivinanzas.

Tengo un UIViewController(FrequencyViewController) que contiene un UITableView(FrequencyTableView). Cuando el usuario toca la fila en questionTableView que contiene la respuesta correcta, se debe crear una instancia de una vista (correctViewController) y su vista debe deslizarse hacia arriba desde la parte inferior de la pantalla, como una vista modal. Esto le dice al usuario que tiene una respuesta correcta y restablece el FrequencyViewController detrás de él listo para la siguiente pregunta. correctViewController se descarta al presionar un botón para revelar la siguiente pregunta.

Todo esto funciona correctamente cada vez, y la vista de la vista correcta de ViewController aparece instantáneamente siempre que lo presentViewController:animated:completionhaya hecho animated:NO.

Si lo configuro animated:YES, correctViewController se inicializa y realiza llamadas a viewDidLoad. Sin embargo viewWillAppear, viewDidAppeary el bloque de finalización de presentViewController:animated:completionno se llaman. La aplicación se queda ahí y sigue mostrando FrequencyViewController hasta que hago un segundo toque. Ahora, se llaman viewWillAppear, viewDidAppear y el bloque de finalización.

Investigué un poco más, y no es solo otro toque lo que hará que continúe. Parece que si inclino o sacudo mi iPhone, esto también puede hacer que active viewWillLoad, etc. Es como si estuviera esperando cualquier otra entrada del usuario antes de que progrese. Esto sucede en un iPhone real y en el simulador, lo cual probé enviando el comando de agitar al simulador.

Realmente no sé qué hacer al respecto ... Realmente agradecería cualquier ayuda que alguien pueda brindar.

Gracias

Aquí está mi código. Es bastante simple ...

Este es el código en questionViewController que actúa como delegado de questionTableView

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
    {
        // If guess was wrong, then mark the selection as incorrect
        NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
        [cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];            
    }
    else
    {
        // If guess was correct, show correct view
        NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        self.correctViewController = [[HFBCorrectViewController alloc] init];
        self.correctViewController.delegate = self;
        [self presentViewController:self.correctViewController animated:YES completion:^(void){
            NSLog(@"Completed Presenting correctViewController");
            [self setUpViewForNextQuestion];
        }];
    }
}

Este es el conjunto de correctViewController

@implementation HFBCorrectViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self)
    {
        // Custom initialization
        NSLog(@"[HFBCorrectViewController initWithNibName:bundle:]");
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSLog(@"[HFBCorrectViewController viewDidLoad]");
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"[HFBCorrectViewController viewDidAppear]");
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)close:(id)sender
{
    NSLog(@"[HFBCorrectViewController close:sender:]");
    [self.delegate didDismissCorrectViewController];
}


@end

Editar:

Encontré esta pregunta antes: UITableView y presentViewController necesitan 2 clics para mostrarse

Y si cambio mi didSelectRowcódigo a este, funciona muy a tiempo con la animación ... Pero es complicado y no tiene sentido por qué no funciona en primer lugar. Entonces no cuento eso como una respuesta ...

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{

    if (indexPath.row != [self.frequencyModel currentFrequencyIndex])
    {
        // If guess was wrong, then mark the selection as incorrect
        NSLog(@"Incorrect Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);
        UITableViewCell *cell = [self.frequencyTableView cellForRowAtIndexPath:indexPath];
        [cell setBackgroundColor:[UIColor colorWithRed:240/255.0f green:110/255.0f blue:103/255.0f alpha:1.0f]];
        // [cell setAccessoryType:(UITableViewCellAccessoryType)]

    }
    else
    {
        // If guess was correct, show correct view
        NSLog(@"Correct Guess: %@", [self.frequencyModel frequencyLabelAtIndex:(int)indexPath.row]);

        ////////////////////////////
        // BELOW HERE ARE THE CHANGES
        [self performSelector:@selector(showCorrectViewController:) withObject:nil afterDelay:0];
    }
}

-(void)showCorrectViewController:(id)sender
{
    self.correctViewController = [[HFBCorrectViewController alloc] init];
    self.correctViewController.delegate = self;
    self.correctViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
    [self presentViewController:self.correctViewController animated:YES completion:^(void){
        NSLog(@"Completed Presenting correctViewController");
        [self setUpViewForNextQuestion];
    }];
}
HalfNormalled
fuente
1
Tengo el mismo problema. Verifiqué con NSLogs que no llamo a ningún método que tarde mucho en ejecutarse. Parece que es solo la animación que presentViewController:se supone que se dispara comienza con un gran retraso. Esto parece ser un error en iOS 7 y también se comenta en los foros de desarrollo de Apple.
Theo
Tengo el mismo problema. Parece un error de iOS7, no ocurre en iOS6. Además, en mi caso, ocurre solo la primera vez después de que abro el controlador de vista de presentación. @Theo, ¿puede proporcionar un enlace a los foros de desarrollo de Apple?
AX

Respuestas:

158

Hoy me he encontrado con el mismo problema. Profundicé en el tema y parece que está relacionado con el runloop principal dormido.

En realidad, es un error muy sutil, porque si tiene la más mínima animación de retroalimentación, temporizadores, etc. en su código, este problema no saldrá a la luz porque estas fuentes mantendrán vivo el runloop. Encontré el problema al usar un UITableViewCellque estaba selectionStyleconfigurado en UITableViewCellSelectionStyleNone, de modo que ninguna animación de selección activó el runloop después de que se ejecutó el controlador de selección de filas.

Para solucionarlo (hasta que Apple haga algo), puede activar el runloop principal por varios medios:

La solución menos intrusiva es llamar CFRunLoopWakeUp:

[self presentViewController:vc animated:YES completion:nil];
CFRunLoopWakeUp(CFRunLoopGetCurrent());

O puede poner un bloque vacío en la cola principal:

[self presentViewController:vc animated:YES completion:nil];
dispatch_async(dispatch_get_main_queue(), ^{});

Es gracioso, pero si agitas el dispositivo, también activará el bucle principal (tiene que procesar los eventos de movimiento). Lo mismo con los toques, pero eso está incluido en la pregunta original :) Además, si el sistema actualiza la barra de estado (por ejemplo, las actualizaciones del reloj, la intensidad de la señal WiFi cambia, etc.), eso también activará el bucle principal y presentará la vista. controlador.

Para cualquier persona interesada, escribí un proyecto de demostración mínimo del problema para verificar la hipótesis de runloop: https://github.com/tzahola/present-bug

También le informé del error a Apple.

Tamás Zahola
fuente
Gracias Tamás. Esa es una gran información. Explica por qué a veces necesitaba tocar para iniciar el ciclo de ejecución o, a veces, simplemente esperaba y funcionaba. He marcado esto como la solución, ya que no parece que podamos hacer nada más hasta que se solucione el error.
HalfNormalled
Apple cava un gran hoyo, caigo en él y me siento mal.
OpenThread
7
¡Dios mío, esto es tan divertido! Por cierto, este error todavía existe.
igrrik
1
Tuve exactamente el mismo problema que fue realmente molesto, afortunadamente esto lo solucionó.
Mark
1
Confirmado que este problema todavía ocurre en iOS 13
Eric Horacek
69

Mira esto: https://devforums.apple.com/thread/201431 Si no quieres leerlo todo, la solución para algunas personas (incluido yo) fue hacer la presentViewControllerllamada explícitamente en el hilo principal:

Rápido 4.2:

DispatchQueue.main.async { 
    self.present(myVC, animated: true, completion: nil)
}

C objetivo:

dispatch_async(dispatch_get_main_queue(), ^{
    [self presentViewController:myVC animated:YES completion:nil];
});

Probablemente iOS7 esté arruinando los hilos didSelectRowAtIndexPath.

HACHA
fuente
wow, esto funcionó para mí. Yo también estaba viendo los retrasos. No puedo entender por qué esto debería ser necesario. Gracias por publicar esto.
drudru
4
Archivado un radar con Apple sobre este problema: rdar: // 19563577 openradar.appspot.com/19563577
mluisbrown
1
Estoy en iOS 8 y esto no me soluciona; siempre se informa que está en el hilo principal, pero todavía veo el problema descrito.
Robert
7

Lo omití en Swift 3.0 usando el siguiente código:

DispatchQueue.main.async { 
    self.present(UIViewController(), animated: true, completion: nil)
}
marca
fuente
1
resuelve el problema. Tuve el mismo problema porque estaba llamando presentpara una devolución de llamada de cierre.
Jeremy Piednoel
2

Llamar [viewController view]al controlador de vista que se presenta fue el truco para mí.

JaganY
fuente
Esto funcionó para mí en iOS 8. Creo que es mejor que dispatch_async.
Robert
0

Tendría curiosidad por ver qué [self setUpViewForNextQuestion];hace.

Puede intentar llamar [self.correctViewController.view setNeedsDisplay];al final de su bloque de finalización en presentViewController.

Mella
fuente
En este momento, setUpViewForNextQuestion solo llama a reloadData en la vista de tabla (para cambiar todos los colores de fondo de nuevo a blanco si alguno se había establecido en rojo por una suposición incorrecta). Pero, ¿algún cambio haría alguna diferencia? El problema es que correctViewController no aparece hasta que el uso vuelve a tocar, por lo que este bloque de finalización no se llama hasta después de ese segundo toque.
HalfNormalled
Probé tu sugerencia, pero no hizo ninguna diferencia. Como digo, la vista no aparece, por lo que el bloque de finalización no se llama hasta después del segundo toque o el gesto de agitar.
HalfNormalled
hmm. para empezar, usaría puntos de interrupción para confirmar que su código de presentación no se llama hasta el segundo toque. (en lugar de ser llamado en el primer toque, pero no aparecer en pantalla hasta más tarde). si esto último es cierto, intente forzar que su presentación ocurra en el hilo principal. Cuando usa "performSelectorWithDelay: 0", eso obliga a la aplicación a esperar hasta la siguiente iteración del ciclo de ejecución antes de ejecutarse. Podría estar relacionado con subprocesos.
Nick
Gracias, Nick. ¿Cómo verificaría usando puntos de interrupción? En este momento estoy usando NSLog para verificar cuándo se llama a cada método. Esto es lo que tengo ahora: Tap 1 Correct Guess: 1 kHz 18:04:57.173 Frequency[641:60b] [HFBCorrectViewController initWithNibName:bundle:] 18:04:57.177 Frequency[641:60b] [HFBCorrectViewController viewDidLoad] Ahora no pasa nada hasta: Tap 218:05:00.515 Frequency[641:60b] [HFBCorrectViewController viewDidAppear] 18:05:00.516 Frequency[641:60b] Completed Presenting correctViewController 18:05:00.517 Frequency[641:60b] [HFBFrequencyViewController setUpViewForNextQuestion]
HalfNormalled
Lo siento, debería haber sido más explícito. Entiendo cómo usar los puntos de interrupción. Recibo la misma historia que con NSLog. Puse puntos de interrupción en viewDidLoad y viewDidAppear en el correctViewController. Se rompe en viewDidLoad, luego, cuando continúo, se sienta y espera otro toque, luego se rompe en viewDidAppear. A veces parece que no necesita un toque y se basa en el tiempo, para cuando he revisado las cosas para ver qué más se está llamando, se trata de llamar a viewDidLoad.
HalfNormalled
0

Escribí la extensión (categoría) con el método swizzling para UIViewController que resuelve el problema. Gracias a AX y NSHipster por las sugerencias de implementación ( swift / aim -c ).

Rápido

extension UIViewController {

 override public class func initialize() {
    struct DispatchToken {
      static var token: dispatch_once_t = 0
    }
    if self != UIViewController.self {
      return
    }
    dispatch_once(&DispatchToken.token) {
      let originalSelector = Selector("presentViewController:animated:completion:")
      let swizzledSelector = Selector("wrappedPresentViewController:animated:completion:")

      let originalMethod = class_getInstanceMethod(self, originalSelector)
      let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

      let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

      if didAddMethod {
        class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
      }
      else {
        method_exchangeImplementations(originalMethod, swizzledMethod)
      }
    }
  }

  func wrappedPresentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
    dispatch_async(dispatch_get_main_queue()) {
      self.wrappedPresentViewController(viewControllerToPresent, animated: flag, completion: completion)
    }
  }
}  

C objetivo

#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(presentViewController:animated:completion:);
        SEL swizzledSelector = @selector(wrappedPresentViewController:animated:completion:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)wrappedPresentViewController:(UIViewController *)viewControllerToPresent 
                             animated:(BOOL)flag 
                           completion:(void (^ __nullable)(void))completion {
    dispatch_async(dispatch_get_main_queue(),^{
        [self wrappedPresentViewController:viewControllerToPresent
                                  animated:flag 
                                completion:completion];
    });

}

@end
Gladkov_Art
fuente
0

Compruebe si su celda en el guión gráfico tiene Selection = none

Si es así, cámbielo a azul o gris y debería funcionar

Mkey
fuente
0

XCode Vesion: 9.4.1, Swift 4.1

En mi caso, esto sucede cuando toco celda y me muevo para la otra vista. Depuraré en lo más profundo y parece que sucede dentro viewDidAppeardebido a que contiene el siguiente código

if let indexPath = tableView.indexPathForSelectedRow {
   tableView.deselectRow(at: indexPath, animated: true)
}

luego agregué el segmento de código anterior dentro prepare(for segue: UIStoryboardSegue, sender: Any?)y funcionó perfecto.

Según mi experiencia, mi solución es, si esperamos hacer algún cambio nuevo (por ejemplo, recargar la tabla, deseleccionar la celda seleccionada, etc.) para la vista de la tabla cuando regrese nuevamente desde la segunda vista, entonces use delegar en lugar de viewDidAppearusar el tableView.deselectRowcódigo anterior segmento antes de mover el segundo controlador de vista

Sachintha Udara
fuente