Bugs lors de l'utilisation du clavier système

En interagissant avec l'application, nous activons à un moment donné le clavier du système pour taper un message ou remplir les champs requis. Avez-vous rencontré des situations où le clavier est affiché, mais il n'y a pas de champ pour entrer un message, ou vice versa - il y a un clavier, où saisir du texte, n'est pas visible? Les bogues peuvent être liés à des problèmes dans une application spécifique, ainsi qu'à des lacunes générales du clavier système.

Konstantin Mordan , un développeur iOS de Mail.ru, a tout vu dans son travail: après avoir analysé les méthodes de contrôle du clavier dans iOS, il a décidé de partager les principaux bugs et approches qu'il a utilisés pour les détecter et les corriger.



Attention: sous la coupe, nous mettons beaucoup de gifs pour bien montrer les bugs. Vous trouverez encore plus d'exemples dans le reportage vidéo de Konstantin sur AppsConf.

Implémentation d'un appel clavier système


Commençons par comprendre comment implémenter un appel au clavier en général.

Imaginez que vous développez une application dont la tâche est d'assembler Aika (un personnage de South Park) en un Canadien entier à l'aide du clavier. Lorsque vous appuyez sur Aiku sur le ventre, le clavier part, soulevant ainsi les jambes de notre héros à la tête.

Pour implémenter la tâche, vous pouvez utiliser InputAccessoryView ou traiter les notifications système.

InputAccessoryView


Regardons la première option.

Dans le ViewController, créez une vue qui montera avec le clavier et donnez-lui un cadre. Il est important que cette vue ne soit pas ajoutée en tant que sous-vue. Ensuite, nous remplaçons les propriétés canBecomeFirstResponder et retournons true. Après avoir redéfini la propriété UIResponder - inputAccessoryView et y mettre la vue. Pour fermer le clavier, ajoutez tapGesture et dans son gestionnaire, réinitialisez le premier Répondeur , que nous avons créé View.

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 ( ) } } 

La tâche est terminée et le système lui-même traite les changements d'état du clavier, l'affiche et ouvre la vue, qui en dépend.



Traitement des notifications système


Dans le cas du traitement des notifications, nous devrons traiter nous-mêmes les notifications des groupes suivants:

  • quand le clavier sera / était affiché: keyboardWillShowNotification, keyboardDidShowNotification;
  • quand le clavier sera / était caché: keyboardWillHideNotification, keyboardDidHideNotification;
  • lorsque le cadre du clavier sera / a été modifié: keyboardWilChangeFrameNotification, keyboardDidChangeFrameNotification.

Pour implémenter notre cas, prenons keyboardWilChangeFrameNotification , car cette notification est envoyée à la fois lorsque le clavier est affiché et lorsqu'il est masqué.

Nous créons un clavierTracker, nous nous y abonnons pour recevoir une notification keyboardWillChangeFrame , et dans le gestionnaire, nous obtenons le cadre du clavier, le convertissons du système de coordonnées d'écran au système de coordonnées de la fenêtre, calculons la hauteur du clavier et modifions la valeur Y de la vue, qui devrait être augmentée par le clavier, à cette hauteur.

 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 ) } } 

Sur ce, notre tâche est terminée, le clavier se lève, ramassant Ike au Canadien.

Comme nous pouvons le voir, la mise en œuvre du travail avec le clavier est assez facile dans les deux cas, donc chacun est libre de choisir la méthode appropriée par lui-même. Dans notre projet, nous avons fait un choix en faveur des notifications, donc d'autres exemples et informations seront associés au traitement des notifications.

Recherche de bugs


Si la façon d'appeler le clavier est si simple, alors d'où viennent les bugs? Bien sûr, si l'application reproduit uniquement le script d'ouverture et de fermeture du clavier, il n'y aura aucun problème. Mais si vous changez le cours habituel des choses, rappelez-vous que non seulement notre application peut utiliser le clavier, mais aussi d'autres, et que l'utilisateur peut également basculer entre eux, alors les surprises ne peuvent être évitées.

Regardons un exemple. Pour ce faire, utilisez notre application avec Ike: ouvrez le clavier, passez à Notes, imprimez quelque chose et revenez à l'application.



Quels problèmes sont déjà visibles? Tout d'abord, il n'y a pas de clavier dans l'App Switcher, bien que lorsque vous avez réduit l'application, c'était, et au lieu de cela, d'autres contenus sont visibles. Deuxièmement, lorsque vous revenez à l'application, le clavier n'est toujours pas là et les jambes d'Ike tombent sur l'écran.

Voyons les raisons de ce comportement. Comme nous nous en souvenons tous du diagramme du cycle de vie de l'application, les transitions d'une application d'un état actif à un état inactif d'abord au premier plan puis en arrière-plan prennent du temps.

Qu'en est-il du cycle de vie du clavier? Sous iOS, pour chaque unité de temps, le clavier ne peut appartenir qu'à une seule des applications en cours d'exécution, mais toutes les applications signées y reçoivent des notifications sur les modifications de l'état du clavier.

Lors du passage d'une application à une autre, le système réinitialise son premier répondeur, qui agit comme un déclencheur pour masquer le clavier. Le système envoie d'abord une notification keyboardWillHide pour que le clavier disparaisse, puis keyboardDidHideNotification. La notification vole vers la deuxième application. Dans la nouvelle application, nous ouvrons le clavier: le système envoie keyboardWillShowNotification pour que le clavier apparaisse, puis envoie le keyboardDidShowNotification - une démo , avec les phases du cycle.



Si vous regardez un fragment du rapport (à partir de 8:39), vous verrez le moment où, après avoir caché le clavier, le système envoie keyboardDidHideNotification pour mettre la première application dans un état inactif. Lorsque vous basculez vers l'application sportive et lancez le clavier, le système envoie keyboardWillShowNotification. Mais comme le processus de commutation et de démarrage est rapide et que le temps de transition entre les phases du cycle de vie peut être plus long, la notification reçue traitera non seulement la demande de sport, mais également la demande de bière, qui n'a pas encore réussi à passer à l'arrière-plan.

Après avoir compris les raisons, trouvons maintenant une solution au problème avec Ike.

Mauvaise décision


La première chose qui me vient à l'esprit est l'idée de se désabonner / s'abonner aux notifications lors de la réduction / maximisation d'une application via activer / désactiver KeyboardTracker.

Pour vous désinscrire, nous utilisons la méthode applicationWillResignActive ou un gestionnaire de notification similaire du système; pour vous abonner, nous utilisons applicationDidBecomeActive, mais pour ne rien manquer, nous notifierons également la méthode applicationWillEnterForeground, qui est appelée lorsque l'application entre au premier plan mais ne devient pas encore active.

Lorsque vous démarrez le clavier dans l'application, tout va probablement réussir, mais avec des tests plus complexes, par exemple, ouvrir le clavier et essayer d'enregistrer la numérotation vocale, la solution ne fonctionnera pas.



Qu'est-il arrivé? Après avoir cliqué sur le bouton de numérotation des messages vocaux, l'application firstResponder a été réinitialisée, le clavier a été fermé, la méthode applicationWillResignActive a été appelée et nous nous sommes désabonnés. Après la fermeture de l'alerte, le système a restauré l'état de l'application, mais avant l'appel de la méthode applicationWillEnterForeground, et en particulier de l'applicationDidBecomeActive.

Bonne décision


Une autre solution est l'utilisation d'un bouillon de protection (Bool).

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

Nous nous souvenons si le clavier a été ouvert avant le sujet, comment l'application a cessé d'être active et dans la méthode applicationWillEnterForeground, nous restaurons l'état précédent. La seule chose qui reste à corriger est le trou dans le sélecteur d'application.



sélecteur d'application


Le sélecteur d'application affiche les instantanés d'application que le système fait une fois l'application mise en arrière-plan. La capture d'écran montre que l'instantané de notre application a été réalisé à un moment où le clavier est déjà utilisé par une autre application. Ce n'est pas critique, mais cela ne prend que quelques clics pour y remédier.

Bonne solution


La solution peut être empruntée à des applications bancaires qui ont appris à cacher des données sensibles, et à lire également auprès d' Apple .

Vous pouvez masquer les données dans la méthode applicationDidEnterBackground, flouter et afficher l'écran de démarrage, et dans la méthode applicationWillEnterForeground, revenir à la hiérarchie de vues habituelle.

Cette option ne nous convient pas, car au moment où la méthode applicationDidEnterBackground est appelée, notre application n'a plus de clavier.

Bonne décision


Nous utiliserons les méthodes familières willResignActive, willEnterForeground et didBecomeActive.

Bien que notre application dispose toujours d'un clavier, vous devrez créer votre propre instantané de l'application dans la méthode willResignActive et le placer dans la hiérarchie.

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

Dans les méthodes willEnterForeground et didBecomeActive, nous restaurons la hiérarchie des vues et supprimons notre instantané.

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

En conséquence, nous avons corrigé les deux cas: dans le sélecteur d'application, une belle image et le clavier ne sautent plus lors de la commutation. Il semblerait que ce ne sont pas des choses aussi importantes, mais pour le développement de produits, ces points sont extrêmement importants.

Mauvaise nouvelle


Notre solution réussie au problème d'Ike concernait le cas où le clavier était ouvert avant de minimiser l'application. Si la commutation se produit sans étendre le clavier, nous verrons à nouveau que les jambes de notre Ike sont tombées en dessous.



Ce n'est pas seulement un problème pour notre application, ce comportement est également observé pour Facebook, qui fonctionne avec les notifications, et même pour iMessage, qui utilise inputAccessoryView pour contrôler le clavier. Cela est dû au fait qu'avant de passer en arrière-plan, les applications parviennent à traiter les notifications du clavier d'autres personnes.

fermeture interactive du clavier


Ajoutez des fonctionnalités à notre application avec Ike, en apprenant au programme à masquer le clavier de manière interactive.



Mauvaise décision


Une façon de rendre cette fonctionnalité consiste à modifier le cadre de la vue du clavier. Nous créons panGestureRecognizer, dans son gestionnaire, nous calculons la nouvelle valeur de la coordonnée Y pour le clavier, en fonction de la position de notre doigt, trouvons la vue du clavier et la mettons à jour avec la valeur de la coordonnée Y.

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

Le clavier est affiché dans une fenêtre distincte, vous devez donc parcourir la totalité du tableau de fenêtres dans l'application, vérifier pour chaque élément du tableau s'il s'agit d'une fenêtre de clavier et, si c'est le cas, en obtenir une vue qui montre le clavier.

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

Malheureusement, cette solution ne fonctionnera pas normalement sur iPhone X et supérieur, car lorsque vous déplacez votre doigt, vous pouvez légèrement toucher l'indicateur inférieur, qui est responsable de la réduction de l'application. Après cela, le masquage interactif cesse de fonctionner.



Le problème réside dans le tableau des fenêtres.



Après le geste, le système crée une nouvelle fenêtre de clavier au-dessus de celle existante. C'est impensable, mais vrai. En conséquence, il s'avère que le tableau contient deux fenêtres de clavier avec les mêmes coordonnées, mais la première est masquée.



Il s'avère que, en parcourant le tableau des fenêtres, nous trouvons la première qui remplit les conditions, et commençons à travailler avec, malgré le fait qu'elle soit cachée.

Comment est-ce résolu? Tourner un tableau de fenêtres.

 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 } 

Fonctionnalités du clavier sur iPad


Le clavier de l'iPad a un état non ancré en plus de son état habituel. L'utilisateur peut le déplacer sur l'écran, le diviser en deux parties et même lancer l'application en mode slide over (au dessus de l'autre). Bien sûr, il est important que dans tous ces modes le clavier fonctionne sans bugs.

Vérifions notre Hayke.



Hélas, ce n'est pas le cas actuellement. Une fois que l'utilisateur a commencé à déplacer le clavier sur l'écran, les jambes d'Ike s'envolent au-dessus de sa tête et n'apparaissent en place qu'après la prochaine ouverture du clavier. Essayons de le réparer sur un cas avec un clavier divisé.

Raisons


Commençons par analyser les notifications. Après avoir cliqué sur le bouton fractionné, nous obtenons deux groupes de notifications - keyboardWillChangeFrameNotification, keyboardWillHideNotification, keyboardDidChangeFrameNotification, keyboardDidHideNotification. La différence entre les groupes réside uniquement dans les coordonnées du clavier.

Lorsque nous cliquons sur le bouton partagé, le clavier diminue et le premier groupe de notifications arrive. Lorsque le clavier s'est séparé et est monté - nous avons reçu un deuxième pack de notifications.

L'important est que nous recevions des notifications indiquant que le clavier a disparu, mais pas qu'il s'affiche. Ceci, soit dit en passant, est un autre avantage en faveur de l'utilisation de keyboardWillChangeFrameNotification.

Pourquoi, alors, les jambes d'Ike s'envolent-elles dès que nous commençons à déplacer le clavier sur l'écran?

À ce stade, le système nous envoie un keyboardWillChangeFrameNotification, mais les coordonnées qui s'y trouvent sont (0,0, 0,0, 0,0, 0,0), car le système ne sait pas à quel point le clavier se trouvera une fois le mouvement terminé.

Si vous substituez des zéros dans le code actuel qui gère la modification du cadre du clavier, il s'avère que la hauteur du clavier est égale à la hauteur de la fenêtre. C’est la raison pour laquelle les jambes d’Ike s’échappent de l’écran.

Bonne décision


Pour résoudre notre problème, nous allons d'abord apprendre à comprendre quand le clavier est en mode non ancré et que l'utilisateur peut le déplacer sur l'écran.

Pour ce faire, il suffit de comparer la hauteur de la fenêtre et le clavier maxY. S'ils sont égaux, alors le clavier dans son état normal, si maxY est inférieur à la hauteur de la fenêtre, alors l'utilisateur déplace le clavier. Par conséquent, le code suivant apparaît dans 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) } } 

Nous avons personnalisé la hauteur à zéro, et maintenant avec le mouvement du clavier, les jambes d'Ike descendent et y sont fixées.



Le seul malentendu qui subsiste est le fait que lors de la division du clavier, les jambes d'Ike ne tombent pas immédiatement. Comment y remédier?

Nous apprendrons à keyboardTracker à fonctionner non seulement avec keyboardWillChangeFrameNotification, mais aussi avec keyboardDidChangeFrame. Vous n'avez pas besoin d'écrire de nouveau code, il suffit d'ajouter une vérification qu'il s'agit d'un iPad afin de ne pas faire de calculs inutiles.

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




Comment détecter les bugs?


Journalisation abondante


Sur notre projet, les journaux sont écrits au format suivant: entre crochets, le nom du module et du sous-module, auquel le journal appartient, puis le texte du journal lui-même. Par exemple, comme ceci:
[keyboard][tracker] keyboardWillChangeFrame: calculated height - 437.9

Dans le code, il ressemble à ceci: un enregistreur est créé avec une balise de niveau supérieur et transmis au tracker. À l'intérieur du tracker, un enregistreur avec une balise de deuxième niveau, qui est utilisé pour se connecter à l'intérieur de la classe, est dérivé de l'enregistreur.

 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)") } } 

J'ai donc promis l'intégralité du keyboardTracker, ce qui est bien. Si les testeurs ont trouvé des problèmes, j'ai pris le fichier journal et j'ai cherché exactement où les cadres ne correspondaient pas. Cela a pris trop de temps, par conséquent, en plus de la journalisation, d'autres méthodes ont commencé à être appliquées.

Chien de garde


Dans notre projet, Watchdog est utilisé pour optimiser le flux d'interface utilisateur. Cela a été dit par Dmitry Kurkin lors de l'un des derniers AppsConf .

Un chien de garde est un processus ou un thread qui surveille un autre processus ou thread. Ce mécanisme vous permet de surveiller l'état du clavier et les vues qui en dépendent et de signaler les problèmes.

Pour implémenter une telle fonctionnalité, nous créons une minuterie qui, une fois par seconde, vérifiera l'emplacement correct de la vue avec les jambes de Hayk ou l'enregistrera en cas d'erreur.

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

Soit dit en passant, vous pouvez enregistrer non seulement les résultats finaux, mais aussi des calculs intermédiaires.

En conséquence, une journalisation abondante + Watchdog a fourni des données précises sur le problème, l'état du clavier et réduit le temps de correction des bogues, mais cela n'a pas aidé les utilisateurs bêta qui ont dû endurer des erreurs jusqu'à la prochaine version.

Mais que se passe-t-il si le chien de garde peut être formé non seulement pour trouver des problèmes, mais aussi pour les résoudre?

Dans le code où le chien de garde conclut que les coordonnées de la vue ne convergent pas, nous ajoutons la méthode fixTummyPosition et mettons automatiquement les coordonnées en place.

Dans cette option, beaucoup d'informations utiles sont accumulées dans mes journaux et les utilisateurs ne remarquent aucun problème visuel. Cela semble être génial, mais maintenant je ne peux pas découvrir de problèmes avec le clavier.

Il permet d'ajouter à la méthode de surveillance la possibilité de générer un cache de test lorsqu'une erreur est détectée. Bien sûr, ce code est ajouté sous la configuration de remout.

Maintenant, après la prochaine version, vous pouvez activer la génération de crash de test et, si un utilisateur a des problèmes avec le clavier, son application plante et grâce aux journaux collectés, vous pouvez corriger les bugs.

Tableau de bord


La dernière astuce que nous avons introduite consiste à envoyer des statistiques au moment où wahtchdog a enregistré les statistiques. Sur la base des données obtenues, nous avons tracé le nombre d'erreurs détectées et après la première itération, le nombre d'opérations a été réduit de quatre fois. Bien sûr, il n'a pas été possible de réduire les problèmes à zéro, mais les principales plaintes des utilisateurs ont cessé.

La semaine prochaine, Saint AppsConf se tiendra à Saint-Pétersbourg, où vous pourrez poser des questions non seulement à Konstantin, mais aussi à de nombreux intervenants de la piste iOS.

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


All Articles