Catégories
Actualités Scolaires

Apporter le meilleur de SwiftUI au code UIKit de notre équipe

Comme à peu près tous nos collègues dans le domaine, les développeurs iOS de Grammarly sont enthousiasmés par SwiftUI. Publié lors de la conférence Apple Worldwide Developers Conference (WWDC) 2019, il représente une avancée majeure dans la prise en charge par Apple de la création d'excellentes expériences utilisateur. Mais autant que nous voulons utiliser SwiftUI pour tout, nous ne pouvons pas. D'une part, les bibliothèques sont encore nouvelles et mettront probablement quelques années à se stabiliser complètement. De plus, SwiftUI est uniquement fourni avec iOS 13+, et nous devons continuer à prendre en charge Grammarly pour les anciennes versions d'iOS. Et enfin, notre code UIKit existant représente un énorme investissement de plusieurs années pour notre équipe – nous ne voulons pas simplement le jeter.

Nous nous sommes donc demandé: comment pouvons-nous prendre les améliorations de SwiftUI qui nous intéressent tellement et les inverser essentiellement pour notre code UIKit? Ce message expliquera comment nous avons examiné ce que SwiftUI a à offrir et utilisé ce que nous avons appris afin d'améliorer notre code d'interface utilisateur et nos processus de développement. Nous parlerons également de certains pièges que nous avons rencontrés lors de l'intégration des aperçus en direct de SwiftUI.

Création d'interfaces utilisateur pour les plates-formes Apple

Avant d'entrer dans le code réel, nous allons donner un bref aperçu des compromis entre les trois principales approches prises en charge par Apple pour la création d'interfaces utilisateur: Interface Builder, UIKit + Auto Layout dans le code et maintenant SwiftUI.

Interface Builder 🛠

Interface Builder est l'éditeur par défaut fourni avec Xcode, le jeu d'outils de développement d'Apple. Il s'agit d'un éditeur WYSIWYG et fournit de nombreux éléments de glisser-déposer, il est donc facile de commencer et de créer une application très simple. Outre les éléments d'interface utilisateur fournis, Interface Builder prend également en charge les propriétés personnalisées et le rendu de vue personnalisé.

Mais plus vous ajoutez de personnalisations, plus vous risquez de rencontrer des instabilités et des plantages. Fait amusant: Interface Builder est antérieur à OSX. Il est apparu pour la première fois en 1986, la même année que je suis né. Vous pouvez imaginer que les approches de la disposition des bâtiments ont quelque peu évolué depuis.

Dans IB, la mise en page n'est organisée d'aucune façon et vous ne pouvez pas incorporer un fichier dans un autre, de sorte que votre application devenant plus complexe, la navigation dans l'éditeur peut devenir très rapide. Voici un aperçu de l'organisation d'IB:

La collaboration à un projet complexe au sein d'IB comporte d'autres défis. Les révisions de code sont difficiles: vous ne modifiez généralement pas les fichiers IB sous forme de texte et les systèmes de contrôle des sources ne rendent pas les différences lisibles par l'homme. Démêler les conflits de fusion peut demander beaucoup de travail, car le XML est généré par la machine et l'ordre peut changer considérablement même après des modifications mineures. Le cycle de vie des objets UIKit chargés à partir de fichiers IB n'est pas trivial, ce qui signifie que vous devez parcourir les méthodes comme init(coder:) et awakeFromNib() pour comprendre ce qui se passe. Et pour les tests locaux, chaque fois que vous apportez des modifications, vous devez ouvrir votre appareil et naviguer vers le bon écran.

UIKit + Disposition automatique dans le code 🧑‍💻

Une autre option consiste à créer l'interface utilisateur entièrement en code à l'aide d'UIKit + Auto Layout. UIKit est construit autour du modèle MVC utilisé pour la logique d'interface utilisateur la plus moderne. Il est facile de réutiliser les vues et vous pouvez créer des composants d'interface utilisateur avec une flexibilité illimitée.

Cependant, la création d'interface utilisateur à partir de code présente de nombreux défis. Il n'y a pas d'aperçu en direct de votre interface, vous devez donc créer et exécuter juste pour voir les modifications. Avec la mise en page automatique, il peut être difficile de raisonner sur ce qui se passera simplement en regardant le code. En fin de compte, vous écrivez du code d'interface utilisateur de forme libre – et après quelques années de support et de maintenance sur une application complexe, tout code de forme libre est susceptible de devenir désordonné.

Interface utilisateur rapide 🚀

Lors de la WWDC 2019, Apple a publié SwiftUI comme la prochaine évolution de la création d'interfaces utilisateur, et la réception a été extrêmement positive. Nous avons même créé une version de une Futurama meme pour plaisanter sur le caractère universel de l'excitation:

SwiftUI est déclaratif, donc les vues sont fonction d'un état. Avec les aperçus en direct, lorsque vous modifiez le code sur le côté gauche, vous voyez immédiatement les changements reflétés dans le simulateur sur le côté droit. Le code SwiftUI est généralement écrit d'une manière qui génère des différences concises dans le contrôle de source. Et la hiérarchie de code correspond à la hiérarchie de la mise en page de l'interface utilisateur, ce qui facilite la lisibilité et la maintenabilité.

Code d'interface utilisateur en transition

SwiftUI a beaucoup à aimer, mais comme il est si nouveau, ses API mettront probablement un certain temps à mûrir et à devenir entièrement stables. Comme de nombreuses équipes d'ingénierie iOS, nous avons également beaucoup de bon code UIKit que nous ne sommes pas pressés de jeter. Nous avons donc cherché des moyens de refactoriser notre code UIKit pour incorporer bon nombre des choses que tout le monde aime à propos de SwiftUI.

Si vous avez créé des interfaces utilisateur dans du code pour une application complexe, vous connaissez probablement beaucoup de problèmes et d'anti-modèles rencontrés par notre équipe. Tout d'abord, les sous-vues sont toutes regroupées dans la même liste, il n'est donc pas clair quelle est la hiérarchie pour nos éléments d'interface utilisateur. En outre, la disposition automatique est entrelacée avec la configuration. le viewDidLoad() La méthode brouille la mise en page avec beaucoup de logique non UI: ouverture des points de terminaison réseau, configuration des sources de données, abonnement aux notifications d'événements système, etc.

Comment pouvons-nous rendre notre code UIKit plus propre et plus facile à gérer, comme SwiftUI? Une façon d'éviter le code de forme libre consiste à utiliser des conventions et des wrappers. UIKit a été conçu pour Objective-C, bien avant Swift, et a beaucoup changé au cours de sa vie. En regardant SwiftUI, nous avons trouvé l'inspiration pour refactoriser notre code UIKIt pour avoir de meilleures valeurs par défaut et des interfaces plus riches. Certaines API peuvent être améliorées à l'aide de génériques Swift, tandis que d'autres peuvent être modifiées pour éviter les défauts par défaut que l'on peut parfois oublier de supprimer (comme la suppression translatesAutoresizingMaskIntoConstraints).

Voici quelques exemples de ce sur quoi notre équipe a travaillé.

Extraire le code de vue

Nous pouvons utiliser des classes de vue personnalisées et remplacer la loadView () dans un contrôleur de vue pour charger des vues spécifiques. Avec un contrôleur de vue générique comme base, nous réduisons le code passe-partout.

Condenser les mises à jour de contenu

On peut définir un fill(with:) et l'utiliser comme un point unique où la vue est remplie de données. Cela rend le code plus facile à lire et moins sujet aux erreurs.

Création de contraintes de mise en page automatique plus propres

Nous avons défini LayoutPins—Qui, en tant que concept, se situe quelque part entre les contraintes de disposition automatique et les anciens masques de redimensionnement automatique. Il est utilisé pour générer des contraintes lors de l'ajout d'une sous-vue (qui fonctionne avec environ 90% du code de mise en page dans notre projet).

Cette énumération simple est ensuite étendue avec des fonctions d'assistance pour produire des règles de disposition composées comme «centrer dans le parent» ou «largeur de remplissage».

Wrappers hiérarchiques

Une bonne chose à propos de SwiftUI est que l'indentation du code correspond à la hiérarchie des vues. Pour obtenir un effet similaire avec UIKit, nous avons introduit un générique addSubview et addArrangedSubview des wrappers qui acceptent les fermetures de configuration pour rendre le code de construction de la vue hiérarchique.

Combiner ces emballages avec LayoutPins nous permet de construire des vues à partir de code comme ceci:

Ajout d'aperçus SwiftUI pour le code d'interface utilisateur existant

L'une des meilleures choses à propos de SwiftUI est sa fonction de prévisualisation en direct, qui montre automatiquement les effets de vos modifications de code sur un appareil simulé dans Xcode. Il remplit les vues avec des données réelles et vous pouvez avoir plusieurs aperçus à la fois. Entre autres avantages, les aperçus en direct facilitent la recherche et la documentation des cas marginaux, avec l'avantage supplémentaire de partager automatiquement cette documentation avec votre équipe, car la configuration de l'aperçu fait partie du code source.

Il y a de nombreux articles sur l'utilisation des aperçus SwiftUI avec UIKit. L'idée est simple: enveloppez simplement votre vue UIKit dans un conteneur SwiftUI, et vous avez terminé. Comme expliqué à la WWDC, l'intégration est simple et peut être facilement réalisée en mettant en œuvre le UIViewRepresentable protocole. Cependant, pour nous, l'intégration n'a pas été aussi simple. Nous expliquerons certains des obstacles au cas où cela pourrait aider votre équipe à déboguer des problèmes similaires.

Cible d'extension → cible du cadre

Le composant d'interface utilisateur principal de Grammarly dans iOS est une extension de clavier et, malheureusement, les cibles d'extension d'application ne prennent pas en charge les aperçus SwiftUI. Seules les cibles d'application et de cadre sont prises en charge. (À noter également: vous ne pouvez pas non plus écrire de tests unitaires pour les cibles d'extension d'application.)

Pour obtenir les aperçus, nous devions d'abord déplacer tout le code de l'interface utilisateur vers une nouvelle cible de framework. La cible d'extension est simplement liée au framework et fait référence au contrôleur de vue principal dans le Info.plist fichier:

Cible statique → cible dynamique

Ensuite, nous avons rencontré un autre problème: les aperçus en direct ne sont pas pris en charge pour les cibles de framework statiques.

Le temps de démarrage est crucial pour le clavier UX, donc pour minimiser la liaison dynamique, nous utilisons le MACH_O_TYPE = staticlib paramètre de construction. Pour que les aperçus en direct fonctionnent, nous avons ajouté un paramètre de construction pour utiliser la liaison dynamique lors du débogage avec le simulateur: MACH_O_TYPE(sdk=iphonesimulator*) = mh_dylib

Aperçus pas si vivants

Jusqu'ici tout va bien: nous avons lancé les aperçus en direct. Mais souvent, après avoir modifié nos vues, Xcode commençait à reconstruire l'ensemble du projet pour appliquer ces modifications, ce qui prenait beaucoup de temps, ce qui entraînait un lent processus de débogage avec les aperçus.
Que se passe-t-il sous le capot? Pour recharger l'aperçu en direct lorsqu'un fichier change, Xcode génère un fichier «patch» qui met à jour les méthodes à la volée. (Voici un article de blog utile avec des détails sur la mise en œuvre.)

Une nouvelle bibliothèque dynamique est créée à partir de ce fichier de correctif et chargée dans le processus de rendu d'aperçu. Mais le problème est que seules les méthodes d'instance peuvent être remplacées de cette façon. Toute modification apportée à init les méthodes ou les initialiseurs de propriété déclenchent une reconstruction complète.

Lorsque vous travaillez avec UIKit, vous pouvez décider de tout configurer dans le init(frame:) , ce qui signifie que Xcode devra être reconstruit pour chaque modification mineure. Pour utiliser les aperçus en direct plus facilement, la solution consiste à tout déplacer dans setupView(), appelez-le depuis l'initialiseur et ne touchez jamais init encore.

Symboles en double dans les dépendances

Lors de l'utilisation des aperçus en direct, une partie de notre code ne fonctionnait pas correctement. Tout d'abord, nous avons remarqué que notre as et is les opérateurs échouaient. Pourquoi?

Rappelons que chaque fois que nous modifions du code, une nouvelle bibliothèque dynamique est chargée dans le moteur de rendu pour patcher les méthodes modifiées. Mais cette bibliothèque est liée à toutes nos dépendances – et si les dépendances sont statiques, nous créons effectivement plusieurs copies de code et de types chaque fois que nous changeons quelque chose. En raison des doublons qui étaient maintenant dans le mélange, nous nous sommes retrouvés avec des résultats imprévisibles lorsque nous avons appliqué des vérifications aux composants. La solution à ce problème était simple: il suffit de rendre toutes les dépendances également dynamiques.

Dépendances du framework introuvables

Au moins, cela semblait facile, mais après avoir rendu toutes les dépendances dynamiques, l'extension ne démarrerait pas du tout. Au lieu de cela, il a imprimé des erreurs de console comme celle-ci:

Pour comprendre ce qui se passe ici, examinons la valeur par défaut du LD_RUNPATH_SEARCH_PATHS paramètre de construction, qui définit l'ensemble des emplacements utilisés par l'éditeur de liens dynamique pour localiser les cadres:

Expliquer:

  • @executable_path/Frameworks sont des frameworks intégrés du bundle d'applications.
  • @executable_path/../../Frameworks sont des cadres intégrés du bundle hôte / conteneur (pertinents pour les extensions d'application).
  • @loader_path/Frameworks sont des cadres intégrés du bundle actuel (cela est surtout important pour les applications Mac où les cadres sont autorisés à intégrer d'autres cadres).

Ces chemins de recherche fonctionnent lorsque notre infrastructure est intégrée dans une application ou une extension, mais dans ce cas, notre infrastructure est chargée dans certaines machines Xcode internes, de sorte que nos dépendances ne sont pas intégrées. Heureusement, nous n'avons besoin d'exécuter notre configuration de débogage du simulateur que sur une machine locale. Donc, pour cette configuration, nous pouvons ajouter des chemins absolus à LD_RUNPATH_SEARCH_PATHS pour lui faire savoir où vivent toutes nos dépendances.

Nous utilisons Carthage pour récupérer et construire toutes les dépendances externes, nous avons donc ajouté $(PROJECT_DIR)/Carthage/Build/iOS. Quant à nos dépendances internes, Xcode les construira toutes automatiquement, et elles peuvent être trouvées dans $(BUILT_PRODUCTS_DIR). Le chemin de recherche final pour notre configuration de débogage d'aperçu en direct ressemble à ceci:

Conclusion

Malgré certaines des ruses que nous avons rencontrées, nous restons très enthousiasmés par SwiftUI lui-même et par la façon dont nous pouvons apporter certaines de ses fonctionnalités les plus avantageuses à notre code existant. Nous espérons que vous avez appris quelque chose que vous pouvez appliquer à votre propre code UIKit!

Grammarly recrute activement pour les développeurs iOS. En savoir plus sur notre pile technologique et vérifiez rôles ouverts. Si vous souhaitez écrire un excellent code d'interface utilisateur qui aide des millions de personnes à écrire sur tous leurs appareils, nous aimerions avoir de vos nouvelles!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *