Partie 3 : Écrire un DSL de mise en page automatique avec la surcharge d'opérateur et les générateurs de résultats de Swift ️
Vous pouvez également regarder cet article ici :
La dernière fois, nous avons construit la partie principale du DSL, permettant à l'utilisateur final d'exprimer des contraintes de manière arithmétique.
Dans cet article, nous développerons cela pour inclure:
- Ancres combinées — taille, centre, horizontalEdges, verticalEdges
- Encarts - permettent aux utilisateurs de définir des encarts sur des ancrages de bord combinés
- Générateur de résultats pour gérer la sortie de différentes ancres, permettant un regroupement et une activation faciles des contraintes.
Nous créons d'abord un type pour contenir une paire d'ancres conformes au protocole LayoutAnchor défini dans la partie 1.
Ici, j'ai envisagé de modifier le LayoutBlock pour contenir un tableau d'ancres. Ensuite, lors de la génération d'une contrainte à partir de 2 de ces blocs, nous pourrions compresser les ancres ensemble et les parcourir, en contraignant les ancres les unes aux autres et en transmettant les constantes/multiplicateurs pertinents.
Il y a 2 inconvénients :
- Même des expressions simples avec des ancres de base renverraient un tableau de contraintes. Cela complique le DSL du point de vue des utilisateurs.
- Un utilisateur peut essayer d'utiliser une ancre composite (avec 2 ou 4 ancres) avec une ancre singulière. Nous pouvons gérer cela en ignorant les ancres supplémentaires. Le compilateur ne produira aucun avertissement. Cependant, ces opérations et les contraintes qui en résultent n'auront aucun sens. Cela a le potentiel d'introduire des bogues frustrants dans le code des utilisateurs finaux - quelque chose que nous voulons éviter !!
Étape 11 : Étendez le protocole LayoutAnchorPair.
Cette extension aura le même objectif que l'extension de protocole LayoutAnchor définie précédemment - elle agit comme un wrapper qui appelle les méthodes du type de base et renvoie les contraintes résultantes.
La principale différence ici est que chaque méthode renvoie un tableau de contraintes, qui combine les contraintes des 2 types LayoutAnchor.
Étant donné que les ancres que nous transmettons à LayoutAnchorPair sont limitées aux types LayoutAnchor, nous pouvons facilement fournir une implémentation par défaut.
De plus, au lieu de constantes, ces méthodes prennent un EdgeInsetPair qui offrira la possibilité de fournir différentes constantes à chacune des contraintes.
Chaque méthode mappe constant1 pour contraindre anchor1 et constant2 sur anchor2.
Étape 13 : Créez des types LayoutAnchor concrets.
Dans les parties 1 et 2, nous n'avons pas eu à créer de types LayoutAnchor concrets puisque nous venons de rendre les NSLayoutAnchors par défaut conformes au protocole LayoutAnchor. Ici cependant, nous devons fournir nos propres ancres conformes au protocole AnchorPair.
Rappel des alias de type utilisés précédemment :
Nous définissons un type imbriqué qui satisfait le protocole EdgeInsetPair. Cela satisfait à l'exigence de type associé à AnchorPair - Inserts. Le type de béton respectant ce protocole sera utilisé lors de l'opération de surcharge pour fixer les encarts.
Nous utilisons des propriétés calculées pour nous conformer au protocole LayoutAnchorPair et EdgeInsetPair. Ces propriétés calculées renvoient les propriétés internes de LayoutAnchorPair et EdgeInsetPair.
Ici, il est important de s'assurer que les constantes fournies par le type inset correspondent aux ancres définies sur cette paire d'ancres. Plus précisément dans le contexte de l'extension définie à la dernière étape, où constant1 est utilisé pour contraindre anchor1 .
Ce protocole "générique" nous permet de définir une extension de protocole valable pour toutes les paires d'ancres. À condition de suivre la règle évoquée ci-dessus. En même temps, nous pouvons utiliser des étiquettes spécifiques à l'ancre plus significatives - bas/haut - en dehors de l'extension. Comme lors de la définition des surcharges d'opérateur.
Une solution alternative impliquerait d'avoir des extensions séparées pour tous les types - ce qui n'est pas trop mal puisque le nombre d'ancres est limité. Mais j'étais trop paresseux ici et j'ai essayé de créer une solution abstraite. Dans tous les cas, il s'agit d'un détail d'implémentation qui est interne à la bibliothèque et qui peut être modifié à l'avenir sans casser les changements.
Veuillez laisser un commentaire si vous pensez qu'un design différent serait optimal.
Plus de points de décision architecturaux :
- Les encarts doivent être des types distincts car ils sont initialisés et utilisés en dehors d'une ancre.
- Les types imbriqués sont utilisés pour protéger l'espace de noms et le garder propre, tout en soulignant le fait qu'une implémentation de type Inset dépend de l'implémentation spécifique de PairAnchor.
Remarque : L'ajout d'encarts n'est pas la même chose que l'ajout de constantes à une expression.
Vous avez peut-être remarqué que nous avons ajouté un opérateur moins à la constante supérieure avant de la renvoyer dans le cadre de l'interface EdgePair. De même, les implémentations XAxisAnchorPair ajoutent un moins à l'ancre de fin. Inverser les constantes signifie que les encarts fonctionneront comme des encarts plutôt que de décaler chaque bord dans la même direction.
Dans l'image de gauche, toutes les ancres de bord de la vue bleue sont définies pour être égales à celle de la vue rouge plus un 40. Il en résulte que la vue bleue est de la même taille mais décalée de 40 le long des deux axes. Si cela a du sens en termes de contraintes, ce n'est pas une opération courante en soi.
La définition d'encarts autour d'une vue ou l'ajout d'un rembourrage autour d'une vue est beaucoup plus courant. Par conséquent, dans le contexte de fournir une API pour des acteurs combinés, cela a plus de sens.
Dans l'image de droite, nous définissons la vue bleue comme étant égale à la vue rouge plus un encart de 40 en utilisant ce DSL. Ceci est réalisé par l'inversion des constantes décrites ci-dessus.
Étape 14 : Développez le View & LayoutGuide pour initialiser les ancres composites.
Tout comme nous l'avons fait auparavant, nous étendons les types View et LayoutGuide avec des propriétés calculées qui initialisent les LayoutBlocks lorsqu'ils sont appelés.
Étape 15 : surchargez les opérateurs +/- pour autoriser les expressions avec des encarts de bord.
Pour les ancrages de bord horizontaux et verticaux, nous voulons que l'utilisateur puisse spécifier des encarts. Pour ce faire, nous étendons le type UIEdgeInsets car il est déjà familier à la plupart des utilisateurs de ce DSL.
L'extension permet l'initialisation avec uniquement des encarts haut/bas ou gauche/droite — le reste étant par défaut à 0.
Nous devons également ajouter une nouvelle propriété à LayoutBlock pour stocker les edgeInsets.
Ensuite, nous surchargeons les opérateurs pour les entrées : LayoutBlock avec UIEdgeInsets .
Nous mappons l'instance de UIEdgeInsets fournie par l'utilisateur au type imbriqué pertinent défini dans le cadre de LayoutAnchorPair concret .
Les paramètres supplémentaires ou incorrects transmis par l'utilisateur dans le cadre du type UIEdgeInset seront ignorés.
Étape 15 : surchargez les opérateurs de comparaison pour définir les relations de contrainte.
Le principe reste le même qu'avant. Nous surchargeons les opérateurs de relations pour les entrées LayoutBlocks et LayoutAnchorPair .
Si un utilisateur fournit une paire d'encarts de bord - nous les utilisons, sinon nous générons une paire d'encarts génériques à partir des constantes LayoutBlocks . La structure d'encart générique est un wrapper, contrairement aux autres encarts, elle ne nie pas l'un des côtés.
Étape 16 : Mise en page des dimensionsAnchorPair
Tout comme une ancre de dimension unique, une paire d'ancres de dimension — widthAnchor et heightAnchor — peut être contrainte à des constantes. Par conséquent, nous devons fournir des surcharges d'opérateurs distinctes pour gérer ce cas d'utilisation.
- Permettre à l'utilisateur du DSL de fixer à la fois la hauteur et la largeur à la même constante - créant un carré.
- Autoriser l'utilisateur du DSL à fixer le SizeAnchorPair à un type CGSize - est plus logique dans la plupart des cas car les vues ne sont pas des carrés.
Étape 17 : Utilisation des générateurs de résultats pour gérer les types [NSLayoutConstraint] et NSLayoutConstraint.
Les ancres composites créent un problème intéressant. L'utilisation de ces ancres dans une expression génère un tableau de contraintes. Cela pourrait devenir désordonné pour l'utilisateur final du DSL.
Nous voulons fournir un moyen de regrouper ces contraintes sans code passe-partout et de les activer efficacement - en une seule fois plutôt qu'individuellement ou par lots séparés.
Introduits dans Swift 5.4, les générateurs de résultats (également appelés générateurs de fonctions) vous permettent de créer un résultat à l'aide de "blocs de construction" implicitement à partir d'une série de composants. En fait, ils constituent le bloc de construction sous-jacent de Swift UI.
Dans ce DSL, le résultat final est un tableau d' objets NSLayoutConstraint .
Nous fournissons des fonctions de génération, qui permettent au générateur de résultats de résoudre des contraintes individuelles et des tableaux de contraintes en un seul tableau. Toute cette logique est cachée à l'utilisateur final du DSL.
J'ai copié la plupart de ces fonctions directement à partir de l' exemple de proposition de générateur de résultats à évolution rapide . J'ai ajouté des tests unitaires pour m'assurer qu'ils fonctionnent correctement dans ce DSL.
Mettre tous ces résultats dans ce qui suit:
Les générateurs de résultats nous permettent également d'inclure un flux de contrôle supplémentaire dans la fermeture, ce qui ne serait pas possible avec un tableau.
C'est tout, merci d'avoir lu ! Cet article a pris plusieurs jours à écrire - donc si vous avez appris quelque chose de nouveau, j'apprécierais un ⭐ sur ce dépôt !
Si vous avez des conseils à me donner, ne soyez pas timide : et partagez votre expérience !
La version finale de ce DSL comprend une ancre à quatre dimensions constituée d'une paire de types AnchorPair …
Vous pouvez trouver tout le code ici: