Erros ao trabalhar com o teclado do sistema

Interagindo com o aplicativo, em algum momento ativamos o teclado do sistema para digitar uma mensagem ou preencher os campos obrigatórios. Você já encontrou situações em que o teclado é exibido, mas não há campo para inserir uma mensagem ou vice-versa - o teclado está lá, onde inserir texto não é visível? Os erros podem estar relacionados a problemas em um aplicativo específico, bem como a deficiências gerais do teclado do sistema.

Konstantin Mordan , desenvolvedor de iOS do Mail.ru, viu tudo em seu trabalho: depois de analisar os métodos de controle de teclado no iOS, ele decidiu compartilhar os principais bugs e abordagens que ele usava para detectá-los e corrigi-los.



Cuidado: por baixo do corte, colocamos muitos gifs para demonstrar claramente os erros. Você encontrará ainda mais exemplos no relatório de vídeo do Konstantin no AppsConf.

Implementando uma Chamada de Teclado do Sistema


Vamos começar entendendo como implementar uma chamada de teclado em geral.

Imagine que você esteja desenvolvendo um aplicativo cuja tarefa é montar Aika (um personagem de South Park) em um canadense inteiro usando o teclado. Quando você pressiona Aiku no estômago, o teclado sai, elevando assim as pernas do nosso herói na cabeça.

Para implementar a tarefa, você pode usar InputAccessoryView ou processar notificações do sistema.

InputAccessoryView


Vamos olhar para a primeira opção.

No ViewController, crie uma View que suba junto com o teclado e forneça um quadro. É importante que essa visualização não seja adicionada como uma subvisão. Em seguida, substituímos as propriedades canBecomeFirstResponder e retornamos true. Depois de redefinirmos a propriedade UIResponder - inputAccessoryView e colocar a View lá. Para fechar o teclado, adicione tapGesture e, em seu manipulador, redefina o firstResponder da View que criamos.

class ViewController: UIViewController { var tummyView: UIView { let frame = CGRect(x: x, y: y, width: width, height: height) let v = TummyView(frame: frame) return v } override var canBecomeFirstResponder: Bool { return true } override var input AccessoryView: UIView? { return tummyView } func tapHandler ( ) { tummyView.resignFirstResponder ( ) } } 

A tarefa é concluída e o próprio sistema processa as alterações de estado do teclado, mostra e eleva a Visualização, que depende dela.



Processamento de notificação do sistema


No caso de processar notificações, teremos que processar as notificações dos seguintes grupos:

  • quando o teclado será / foi mostrado: keyboardWillShowNotification, keyboardDidShowNotification;
  • quando o teclado ficará / estava oculto: keyboardWillHideNotification, keyboardDidHideNotification;
  • quando o quadro do teclado será / foi alterado: keyboardWilChangeFrameNotification, keyboardDidChangeFrameNotification.

Para implementar nosso caso, vamos usar keyboardWilChangeFrameNotification , pois essa notificação é enviada quando o teclado é mostrado e quando está oculto.

Criamos um keyboardTracker, nele assinamos para receber uma notificação keyboardWillChangeFrame e, no manipulador, obtemos o quadro do teclado, convertemos do sistema de coordenadas da tela para o sistema de coordenadas da janela, calculamos a altura do teclado e alteramos o valor Y da tela, que deve ser aumentada pelo teclado para esta altura.

 class KeyboardTracker { func enable ( ) { notificationCenter.add0observer(self, seletor: #selector( keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } func keyboardWillChangeFrame ( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo [ UIResponder.keyboardFrameEndUserInfoKey ] as! NSValue ) .cgRectValue let keyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil ) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY delegate.keyboardWillChange ( keyboardHeight ) } } 

Com isso, nossa tarefa está concluída, o teclado sobe, coletando Ike no canadense.

Como podemos ver, a implementação do trabalho com o teclado é bastante fácil nos dois casos, para que todos sejam livres para escolher o método apropriado por conta própria. Em nosso projeto, fizemos uma escolha a favor das notificações, para que mais exemplos e percepções sejam associados ao processamento das notificações.

Procurando por bugs


Se a maneira de chamar o teclado é tão simples, de onde vêm os erros? Obviamente, se o aplicativo reproduzir apenas o script para abrir e fechar o teclado, não haverá problemas. Mas se você alterar o curso normal das coisas, lembre-se de que não apenas nosso aplicativo pode usar o teclado, mas também outros, e o usuário também pode alternar entre eles, então surpresas não podem ser evitadas.

Vejamos um exemplo. Para fazer isso, use nosso aplicativo com Ike: abra o teclado, mude para o Notes, imprima algo e volte ao aplicativo.



Que problemas já são visíveis? Em primeiro lugar, não há teclado no App Switcher, embora quando você minimizasse o aplicativo e, em vez dele, outro conteúdo estivesse visível. Em segundo lugar, quando você retorna ao aplicativo, o teclado ainda não está lá e as pernas de Ike caem na tela.

Vejamos os motivos desse comportamento. Como todos lembramos no diagrama do ciclo de vida do aplicativo, a transição de um aplicativo de um estado ativo para um estado inativo primeiro em primeiro plano e depois em segundo plano leva tempo.

E o ciclo de vida do teclado? No iOS, para cada unidade de tempo, o teclado pode pertencer a apenas um dos aplicativos em execução, mas as notificações sobre alterações no status do teclado são recebidas por todos os aplicativos assinados neles.

Ao alternar de um aplicativo para outro, o sistema redefine seu firstResponder, que atua como um gatilho para ocultar o teclado. O sistema envia uma notificação keyboardWillHide primeiro para que o teclado desapareça e, em seguida, keyboardDidHideNotification. A notificação voa para o segundo aplicativo. No novo aplicativo, abrimos o teclado: o sistema envia keyboardWillShowNotification para o teclado aparecer e, em seguida, envia o keyboardDidShowNotification - uma demonstração , com as fases do ciclo.



Se você olhar um trecho do relatório (das 8:39), verá o momento em que, após ocultar o teclado, o sistema envia keyboardDidHideNotification para colocar o primeiro aplicativo em um estado inativo. Quando você alterna para o aplicativo esportivo e inicia o teclado, o sistema envia keyboardWillShowNotification. Mas como o processo de alternância e início é rápido e o tempo de transição entre as fases do ciclo de vida pode ser mais longo, a notificação recebida processará não apenas o aplicativo de esportes, mas também o aplicativo de cerveja, que ainda não conseguiu ir ao fundo.

Tendo descoberto os motivos, vamos agora encontrar uma solução para o problema com o Ike.

Má decisão


A primeira coisa que vem à mente é a idéia de cancelar a inscrição / assinar notificações ao minimizar / maximizar um aplicativo por meio da ativação / desativação do KeyboardTracker.

Para cancelar a inscrição, usamos o método applicationWillResignActive ou um manipulador de notificação semelhante do sistema; para assinar, usamos applicationDidBecomeActive, mas para não perder nada, também notificaremos o método applicationWillEnterForeground, que é chamado quando o aplicativo entra em primeiro plano, mas ainda não se torna ativo.

Quando você inicia o teclado no aplicativo, provavelmente tudo terá êxito, mas com testes mais complexos, por exemplo, abrindo o teclado e tentando gravar a discagem por voz, a solução não funcionará.



O que aconteceu Depois de clicar no botão de discagem de mensagem de voz, o aplicativo firstResponder foi redefinido, o teclado foi fechado, o método applicationWillResignActive foi chamado e cancelamos a inscrição. Depois de fechar o alerta, o sistema restaurou o estado do aplicativo, mas antes do método applicationWillEnterForeground e, especialmente, o applicationDidBecomeActive, foi chamado.

Boa decisão


Outra solução é o uso de um caldo de proteção (Bool).

 var wasTummyViewFirstResponderBeforeApp0idEnterBackground func willResignActive( notification: NSNotification) { wasTextFieldFirstResponderBeforeAppDidEnterBackground = tummyView.isFirstResponder } func willEnterForeground ( notification: NSNotification) { if wasTextFieldFirstResponderBeforeAppDidEnterBackground { UIView.performWithourAnimation { tummyView.becomeFirstResponder ( ) } } } 

Lembramos se o teclado foi aberto antes do tópico, como o aplicativo deixou de estar ativo e, no método applicationWillEnterForeground, restauramos o estado anterior. A única coisa que resta a corrigir é o buraco no alternador de aplicativos.



alternador de aplicativos


O alternador de aplicativos exibe instantâneos de aplicativos que o sistema faz depois que o aplicativo entra em segundo plano. A captura de tela mostra que o instantâneo de nosso aplicativo foi feito no momento em que o teclado já está sendo usado por outro aplicativo. Isso não é crítico, mas são necessários apenas alguns cliques para corrigi-lo.

Boa solução


A solução pode ser emprestada de aplicativos bancários que aprenderam a ocultar dados confidenciais e também ler na Apple .

Você pode ocultar os dados no método applicationDidEnterBackground, desfocar e mostrar a tela inicial e, no método applicationWillEnterForeground, retornar à hierarquia de exibição usual.

Essa opção não é adequada para nós, porque quando o método applicationDidEnterBackground é chamado, nosso aplicativo não tem mais um teclado.

Boa decisão


Usaremos os métodos familiares willResignActive, willEnterForeground e didBecomeActive.

Embora nosso aplicativo ainda tenha um teclado, você precisará criar seu próprio instantâneo do aplicativo no método willResignActive e colocá-lo na hierarquia.

 func willResignActive( notificaton: NSNotification) { let keyWindow = UIApplication.shared.keyWindow imageView = UIImageView( frame: keyWindow.bounds) imageView.image = snapshot ( ) let lastSubview = keyWindow.subviews.last lastSubview( imageView) } 

Nos métodos willEnterForeground e didBecomeActive, restauramos a hierarquia de visualizações e excluímos nosso instantâneo.

 func willEnterForeground( notification: NSNotification) { imageView.removeFromSuperview( ) } func didBecomeActive( notification: NSNotification) { imageView.removeFromSuperview( ) } 

Como resultado, corrigimos os dois casos: no alternador de aplicativos, uma imagem bonita e o teclado não saltam mais ao alternar. Parece que essas coisas não são tão importantes, mas, para o desenvolvimento do produto, esses pontos são extremamente importantes.

Más notícias


Nossa solução bem-sucedida para o problema de Ike dizia respeito ao caso em que o teclado foi aberto antes de minimizar o aplicativo. Se a troca ocorrer sem expandir o teclado, novamente veremos que as pernas do nosso Ike caíram abaixo.



Isso não é apenas um problema para o nosso aplicativo, esse comportamento também é observado no Facebook, que funciona com notificações, e até no iMessage, que usa inputAccessoryView para controlar o teclado. Isso ocorre porque, antes de mudar para o segundo plano, os aplicativos conseguem processar as notificações de teclado de outras pessoas.

demitir interativamente o teclado


Adicione alguma funcionalidade ao nosso aplicativo com o Ike, ensinando o programa a ocultar interativamente o teclado.



Má decisão


Uma maneira de fazer essa funcionalidade é alterar o quadro da visualização do teclado. Criamos panGestureRecognizer, em seu manipulador calculamos o novo valor da coordenada Y do teclado, dependendo da posição do nosso dedo, localizamos a visualização do teclado e a atualizamos com o valor da coordenada Y.

 func panGestureHandler( ) { let yPosition: CGFloat = value keyboardView( )?.frame.origin.y = yPosition } 

O teclado é exibido em uma janela separada; portanto, você precisa percorrer toda a matriz de janelas no aplicativo, verificar cada elemento da matriz se é uma janela do teclado e, se for o caso, obter uma visualização mostrando o teclado.

 func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

Infelizmente, esta solução não funcionará normalmente no iPhone X e superior, pois quando você move o dedo, você pode tocar levemente no indicador inferior, responsável por minimizar o aplicativo. Depois disso, a ocultação interativa para de funcionar.



O problema está na matriz de janelas.



Após o gesto, o sistema cria uma nova janela do teclado em cima da existente. É impensável, mas é verdade. Como resultado, a matriz contém duas janelas de teclado com as mesmas coordenadas, mas a primeira está oculta.



Acontece que, iterando sobre o conjunto de janelas, encontramos o primeiro que satisfaz as condições e começamos a trabalhar com ele, apesar de estar oculto.

Como isso é corrigido? Transformando uma matriz de janelas.

 func panGeastureHandler( ) { let yPosition: CGFloat = 0.0 keyboardView( )?.frame.origin.y = yPosition } func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows.reversed( ) let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view } 

Recursos do teclado no iPad


O teclado do iPad tem um estado desencaixado, além do estado habitual. O usuário pode movê-lo pela tela, dividi-lo em duas partes e até iniciar o aplicativo no modo slide over (em cima do outro). Obviamente, é importante que em todos esses modos o teclado funcione sem erros.

Vamos verificar o nosso Hayke.



Infelizmente, este não é o caso agora. Depois que o usuário começa a mover o teclado pela tela, as pernas de Ike voam acima da cabeça e aparecem no lugar somente após a próxima abertura do teclado. Vamos tentar consertá-lo em um estojo com um teclado dividido.

Razões


Vamos começar analisando as notificações. Depois de clicar no botão de divisão, obtemos dois grupos de notificações - keyboardWillChangeFrameNotification, keyboardWillHideNotification, keyboardDidChangeFrameNotification, keyboardDidHideNotification. A diferença entre os grupos está apenas nas coordenadas do teclado.

Quando clicamos no botão de divisão, o teclado diminui e o primeiro grupo de notificações chega. Quando o teclado se dividiu e subiu - recebemos um segundo pacote de notificações.

O importante é que recebamos notificações de que o teclado desapareceu, mas não o que é exibido. A propósito, esse é outro fator a favor do uso de keyboardWillChangeFrameNotification.

Por que, então, as pernas de Ike voam para longe assim que começamos a mover o teclado pela tela?

Nesse momento, o sistema nos envia uma keyboardWillChangeFrameNotification, mas as coordenadas que existem existem (0,0, 0,0, 0,0, 0,0), pois o sistema não sabe em que ponto o teclado estará após a conclusão do movimento.

Se você substituir zeros no código atual que lida com a alteração do quadro do teclado, a altura do teclado é igual à altura da janela. Essa é a razão pela qual as pernas de Ike voam para fora da tela.

Boa decisão


Para resolver nosso problema, primeiro aprenderemos a entender quando o teclado estiver no modo desencaixado e o usuário poderá movê-lo pela tela.

Para fazer isso, basta comparar a altura da janela e o teclado maxY. Se forem iguais, o teclado em seu estado normal, se maxY for menor que a altura da janela, o usuário moverá o teclado. Como resultado, o seguinte código aparece no keyboardTracker:

 class KeyboardTracker { func enable( ) { notificationCenter.addObserver( self, selector:#selector( keyboardWillChangeFrame), name:UIResponder.keyboardWillChangeFrameNotification, object:nil) } func keyboardWillChangeFrame( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue let leyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY let isKeyboardUnlocked = isIPad ( ) && keyboardFrame/maxY < windowHeight if isKeyboardUnlocked { keyboardHeight = 0.0 } delegate.keyboardWillChange ( keyboardHeight) } } 

Definimos a altura como zero e, agora, com o movimento do teclado, as pernas de Ike se abaixam e são fixadas lá.



O único mal-entendido restante é o fato de que, ao dividir o teclado, as pernas de Ike não caem imediatamente. Como consertar isso?

Ensinaremos o keyboardTracker a trabalhar não apenas com keyboardWillChangeFrameNotification, mas também com keyboardDidChangeFrame. Você não precisa escrever um novo código; basta verificar se este é um iPad para não fazer cálculos desnecessários.

 class KeyboardTracker { func keyboardDidChangeFrame( notification: NSNotification) { if isIPad ( ) == false { return } 




Como detectar bugs?


Registro abundante


Em nosso projeto, os logs são gravados no seguinte formato: entre colchetes, o nome do módulo e do submódulo ao qual o log pertence e, em seguida, o texto do próprio log. Por exemplo, assim:
[keyboard][tracker] keyboardWillChangeFrame: calculated height - 437.9

No código, tem a seguinte aparência - um criador de logs é criado com uma etiqueta de nível superior e transmitido ao rastreador. Dentro do rastreador, um criador de logs com um tag de segundo nível, usado para o registro dentro da classe, é desmembrado do criador de logs.

 class KeyboardTracker { init(with logger: Logger) { self.trackerLogger = logger.dequeue(withTag: "[tracker]") } func keyboardWillChangeFrame(notification: NSNotification) { let height = 0.0 trackerLogger.debug("\(#function): calculated height - \(height)") } } 

Então, prometi todo o keyboardTracker, o que é bom. Se os testadores encontrarem problemas, peguei o arquivo de log e procurei exatamente onde os quadros não se encaixavam. Isso levou muito tempo, portanto, além do log, outros métodos começaram a ser aplicados.

Cão de guarda


Em nosso projeto, o Watchdog é usado para otimizar o fluxo da interface do usuário. Isso foi dito por Dmitry Kurkin em um dos últimos AppsConf .

Um cão de guarda é um processo ou segmento que observa outro processo ou segmento. Esse mecanismo permite monitorar o status do teclado e as visualizações que dependem dele e relatar problemas.

Para implementar essa funcionalidade, criamos um timer que, uma vez por segundo, verifica o local correto da vista com as pernas de Hayk ou registra-o se houver erro.

 class Watchdog { var timer: Timer? func start ( ) { timer = Timer ( timeInterval: 1.0, repeats: true, block: { ( timer ) in self.woof ( ) } ) } } 

A propósito, você pode registrar não apenas os resultados finais, mas também cálculos intermediários.

Como resultado, o registro abundante + Watchdog forneceu dados precisos sobre o problema, o estado do teclado e reduziu o tempo para corrigir bugs, mas pouco ajudou os usuários beta que tiveram que suportar erros até o próximo lançamento.

Mas e se o cão de guarda puder ser treinado não apenas para encontrar problemas, mas também para corrigi-los?

No código em que o watchdog conclui que as coordenadas da exibição não convergem, adicionamos o método fixTummyPosition e automaticamente colocamos as coordenadas no lugar.

Nesta opção, muitas informações úteis são acumuladas nos meus logs e os usuários nem percebem problemas visuais. Parece ótimo, mas agora não consigo encontrar nenhum problema com o teclado.

Isso ajuda a adicionar ao método watchdog a capacidade de gerar um cache de teste quando um erro é detectado. Obviamente, esse código é adicionado na configuração do remout.

Agora, após o próximo lançamento, você pode ativar a geração de falhas de teste e, se um usuário tiver problemas com o teclado, o aplicativo falhará e, graças aos logs coletados, poderá corrigir os erros.

Dashboard


O último truque que introduzimos é o envio de estatísticas no momento em que o wahtchdog registrou as estatísticas. Com base nos dados obtidos, plotamos o número de erros detectados e após a primeira iteração, o número de operações foi reduzido em quatro vezes. Obviamente, não foi possível reduzir os problemas a zero, mas as principais reclamações dos usuários cessaram.

Na próxima semana, o Saint AppsConf será realizado em São Petersburgo, onde você poderá fazer perguntas não apenas à Konstantin, mas também a inúmeros oradores da faixa iOS.

Source: https://habr.com/ru/post/pt462515/


All Articles