presentViewController: animado: a visualização SIM não aparecerá até que o usuário toque novamente

93

Estou tendo um comportamento estranho com presentViewController:animated:completion. O que estou fazendo é essencialmente um jogo de adivinhação.

Eu tenho um UIViewController(frequencyViewController) contendo um UITableView(frequencyTableView). Quando o usuário toca na linha em questionTableView que contém a resposta correta, uma visualização (correctViewController) deve ser instanciada e sua visualização deve deslizar para cima da parte inferior da tela, como uma visualização modal. Isso informa ao usuário que ele tem uma resposta correta e redefine o frequencyViewController atrás dele, pronto para a próxima pergunta. correctViewController é dispensado ao pressionar um botão para revelar a próxima pergunta.

Isso tudo funciona corretamente todas as vezes, e a visualização do corretoViewController aparece instantaneamente, desde que presentViewController:animated:completiontenha animated:NO.

Se eu definir animated:YES, correctViewController é inicializado e faz chamadas para viewDidLoad. No entanto viewWillAppear, viewDidAppeare o bloco de conclusão de presentViewController:animated:completionnão são chamados. O aplicativo fica lá, ainda mostrando o frequencyViewController, até que eu faça um segundo toque. Agora, viewWillAppear, viewDidAppear e o bloco de conclusão são chamados.

Investiguei um pouco mais, e não é apenas outro toque que fará com que continue. Parece que se eu inclinar ou sacudir meu iPhone, isso também pode fazer com que ele acione o viewWillLoad etc. É como se ele estivesse esperando por qualquer outra entrada do usuário antes de prosseguir. Isso acontece em um iPhone real e no simulador, o que eu provei enviando o comando shake para o simulador.

Estou realmente sem saber o que fazer sobre isso ... Eu realmente aprecio qualquer ajuda que alguém possa fornecer.

obrigado

Aqui está meu código. É bem simples ...

Este é o código em questionViewController que atua como delegado para 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 é todo o 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:

Eu encontrei esta pergunta antes: UITableView e presentViewController levam 2 cliques para exibir

E se eu mudar meu didSelectRowcódigo para isso, ele funciona muito bem com animação ... Mas é confuso e não faz sentido por que não funciona em primeiro lugar. Então eu não considero isso uma resposta ...

- (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
fonte
1
Eu tenho o mesmo problema. Verifiquei com NSLogs que não chamo nenhum método que leve muito tempo para executar. Parece que apenas a animação que presentViewController:deve ser acionada começa com um grande atraso. Este parece ser um bug no iOS 7 e também é discutido no Apple Dev Forums.
Theo
Eu tenho o mesmo problema. Parece um bug do iOS7 - não acontece no iOS6. Além disso, no meu caso, isso acontece apenas na primeira vez depois que abro o controlador de visualização de apresentação. @Theo, você pode fornecer um link para o Apple Dev Forums?
AX

Respostas:

158

Eu encontrei o mesmo problema hoje. Eu cavei no tópico e parece que está relacionado ao runloop principal estar dormindo.

Na verdade, é um bug muito sutil, porque se você tiver o menor feedback de animação, temporizadores, etc. em seu código, esse problema não aparecerá porque o loop de execução será mantido vivo por essas fontes. Eu encontrei o problema usando um UITableViewCellque tinha seu selectionStyledefinido como UITableViewCellSelectionStyleNone, de modo que nenhuma animação de seleção acionou o loop de execução depois que o manipulador de seleção de linha foi executado.

Para consertar (até que a Apple faça algo), você pode acionar o loop de execução principal de vários meios:

A solução menos intrusiva é chamar CFRunLoopWakeUp:

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

Ou você pode enfileirar um bloco vazio na fila principal:

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

É engraçado, mas se você sacudir o dispositivo, ele também acionará o loop principal (ele deve processar os eventos de movimento). O mesmo acontece com os toques, mas isso está incluído na pergunta original :) Além disso, se o sistema atualizar a barra de status (por exemplo, as atualizações do relógio, a intensidade do sinal WiFi muda, etc.), isso também ativará o loop principal e apresentará a visualização controlador.

Para qualquer pessoa interessada, escrevi um projeto de demonstração mínima do problema para verificar a hipótese de loop de execução: https://github.com/tzahola/present-bug

Também relatei o bug à Apple.

Tamás Zahola
fonte
Obrigado Tamás. Essa é uma ótima informação. Isso explica por que às vezes eu precisava tocar para iniciar o loop de corrida, ou às vezes apenas esperar e ele funcionaria. Marquei isso como a solução, pois parece que não há mais nada que possamos fazer até que o bug seja corrigido.
HalfNormalled
A Apple cava um grande buraco, eu caio nele e me sinto muito ferido.
OpenThread
7
OMG, isso é tão hilário! BTW, esse bug ainda existe.
igrrik
1
Eu tive exatamente o mesmo problema que era realmente irritante, felizmente isso resolveu.
Marcos
1
Confirmado que este problema ainda ocorre no iOS 13
Eric Horacek
69

Confira: https://devforums.apple.com/thread/201431 Se você não quiser ler tudo - a solução para algumas pessoas (inclusive eu) foi fazer a presentViewControllerchamada explicitamente no thread principal:

Swift 4.2:

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

Objective-C:

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

Provavelmente iOS7 está bagunçando os fios didSelectRowAtIndexPath.

MACHADO
fonte
uau - isso funcionou para mim. Eu também estava vendo os atrasos. Não consigo entender por que isso deve ser necessário. Obrigado por postar isso.
drudru
4
Arquivado um radar com a Apple sobre este problema: rdar: // 19563577 openradar.appspot.com/19563577
mluisbrown
1
Estou no iOS 8 e isso não corrige para mim - está sempre relatando estar no thread principal, mas ainda vejo o problema descrito.
Robert,
7

Eu o ignorei no Swift 3.0 usando o seguinte código:

DispatchQueue.main.async { 
    self.present(UIViewController(), animated: true, completion: nil)
}
Marca
fonte
1
resolver o problema. Eu tive o mesmo problema porque estava ligando presentpara um retorno de chamada de encerramento.
Jeremy Piednoel
2

Chamar [viewController view]o controlador de visualização que está sendo apresentado funcionou para mim.

JaganY
fonte
Isso funcionou para mim no iOS 8. Acho que é melhor do que dispatch_async.
Robert,
0

Eu estaria curioso para ver o que [self setUpViewForNextQuestion];faz.

Você pode tentar ligar [self.correctViewController.view setNeedsDisplay];no final do seu bloco de conclusão em presentViewController.

usuario
fonte
No momento, setUpViewForNextQuestion apenas chama reloadData no tableview (para alterar todas as cores de fundo de volta para branco se alguma tiver sido definida para vermelho por uma estimativa incorreta). Mas alguma mudança faria alguma diferença? O problema é que correctViewController não aparece até que o uso toque novamente, portanto, este bloco de conclusão não é chamado até depois desse segundo toque.
HalfNormalled
Eu tentei sua sugestão, mas não fez nenhuma diferença. Como eu disse, a visualização não está aparecendo, então o bloco de conclusão não é chamado até depois do segundo toque, ou do gesto de sacudir.
HalfNormalled
Hmm. para começar, eu usaria pontos de interrupção para confirmar que seu código de apresentação não é chamado até o segundo toque. (versus ser chamado no primeiro toque, mas não ser desenhado na tela até algum tempo depois). se o último for verdadeiro, tente forçar sua apresentação a ocorrer no thread principal. Quando você usa "performSelectorWithDelay: 0", isso força o aplicativo a esperar até a próxima iteração do loop de execução antes de executar. Pode ser relacionado ao threading.
Nick
Obrigado, Nick. Como eu verificaria usando pontos de interrupção? No momento, estou usando o NSLog para verificar quando cada método está sendo chamado. Isto é o que eu tenho agora: 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] Agora nada acontece até: 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
Desculpe, deveria ter sido mais explícito. Eu entendo como usar pontos de interrupção. Recebo a mesma história do NSLog. Coloquei pontos de interrupção em viewDidLoad e viewDidAppear no correctViewController. Ele quebra em viewDidLoad, então quando eu continuo, ele se senta e espera por outro toque, então quebra em viewDidAppear. Às vezes, parece não precisar de um toque e é baseado no tempo, no momento em que analiso as coisas olhando o que mais está sendo chamado, ele começa a chamar viewDidLoad.
HalfNormalled
0

Eu escrevi extensão (categoria) com método swizzling para UIViewController que resolve o problema. Agradecimentos a AX e NSHipster pelas dicas de implementação ( swift / objetiva-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)
    }
  }
}  

Objective-C

#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
fonte
0

Verifique se a sua célula no storyboard tem Seleção = nenhum

Se sim, mude para azul ou cinza e deve funcionar

Mkey
fonte
0

XCode Vesion: 9.4.1, Swift 4.1

No meu caso isso acontece, quando toco no celular e passo para a outra visualização. Eu depuro mais profundamente e parece que isso acontece internamente viewDidAppearporque contém o seguinte código

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

em seguida, adicionei o segmento de código acima prepare(for segue: UIStoryboardSegue, sender: Any?)e funcionou perfeitamente.

Na minha experiência, minha solução é, se quisermos fazer novas alterações (por exemplo, recarregar a tabela, desmarcar a célula selecionada etc.) para o tableview quando voltar da segunda visualização, então use delegate em vez de viewDidAppearusar o tableView.deselectRowcódigo acima segmento antes de mover o segundo controlador de vista

Sachintha Udara
fonte