Parte 3: escribir un DSL de diseño automático con la sobrecarga de operadores de Swift y los generadores de resultados ️

Nov 26 2022
También puede ver este artículo aquí: La última vez construimos la parte principal del DSL, lo que permite al usuario final expresar restricciones de forma aritmética. En este artículo ampliaremos esto para incluir: Primero, creamos un tipo para contener un par de anclas que se ajusten al protocolo LayoutAnchor definido en la Parte 1.

También puedes ver este artículo aquí:

La última vez creamos la parte principal del DSL, lo que permite al usuario final expresar las restricciones de forma aritmética.

En este artículo ampliaremos esto para incluir:

  • Anclajes combinados: tamaño, centro, bordes horizontales, bordes verticales
  • Inserciones: permite a los usuarios establecer inserciones en anclajes de borde combinados
  • Result Builder para manejar la salida de diferentes anclas, lo que permite una fácil activación y procesamiento por lotes de restricciones.

Primero, creamos un tipo para contener un par de anclas que se ajusten al protocolo LayoutAnchor definido en la Parte 1.

Aquí consideré modificar LayoutBlock para contener una matriz de anclas. Luego, al generar una restricción a partir de 2 de esos bloques, podríamos juntar los anclajes e iterar sobre ellos, restringiendo los anclajes entre sí y pasando las constantes/multiplicadores relevantes.

Hay 2 desventajas:

  • Incluso las expresiones individuales con anclas básicas devolverían una serie de restricciones. El complica el DSL desde el punto de vista de los usuarios.
  • Un usuario podría intentar usar un ancla compuesta (con 2 o 4 anclas) con un ancla singular. Podemos manejar esto ignorando los anclajes adicionales. El compilador no producirá ninguna advertencia. Sin embargo, estas operaciones y las restricciones resultantes no tendrán sentido. Esto tiene el potencial de introducir errores frustrantes en el código de los usuarios finales, ¡algo que queremos evitar!

Paso 11: Ampliar el protocolo LayoutAnchorPair.

Esta extensión tendrá el mismo propósito que la extensión del protocolo LayoutAnchor definida anteriormente: actúa como un contenedor que llama a los métodos del tipo base y devuelve las restricciones resultantes.

La diferencia clave aquí es que cada método devuelve una matriz de restricciones, que combina las restricciones de los 2 tipos LayoutAnchor.

Debido a que los anclajes que pasamos a LayoutAnchorPair están restringidos a los tipos de LayoutAnchor, podemos proporcionar una implementación predeterminada fácilmente.

Además, en lugar de constantes, estos métodos toman un EdgeInsetPair que ofrecerá la capacidad de proporcionar diferentes constantes para cada una de las restricciones.

Cada método mapea constante1 para restringir ancla1 y constante2 en ancla2.

Paso 13: Cree tipos de LayoutAnchor concretos.

En las Partes 1 y 2 no tuvimos que crear tipos de LayoutAnchor concretos, ya que solo hicimos que los NSLayoutAnchors predeterminados se ajustaran al protocolo LayoutAnchor. Aquí, sin embargo, debemos proporcionar nuestras propias anclas que se ajusten al protocolo AnchorPair.

Un recordatorio de los alias tipográficos utilizados anteriormente:

Definimos un tipo anidado que satisface el protocolo EdgeInsetPair. Esto satisface el requisito de tipo asociado de AnchorPair: recuadros. El tipo de hormigón que se adhiera a este protocolo se utilizará durante la operación de sobrecarga para establecer los insertos.

Usamos propiedades calculadas para cumplir con el protocolo LayoutAnchorPair y EdgeInsetPair. Estas propiedades calculadas devuelven las propiedades internas de LayoutAnchorPair y EdgeInsetPair.

Aquí es importante asegurarse de que las constantes proporcionadas por el tipo de inserción coincidan con los anclajes definidos en este par de anclajes. Específicamente en el contexto de la extensión definida en el último paso, donde se usa constant1 para restringir ancla1 .

Este protocolo "genérico" nos permite definir una extensión de protocolo que es válida para todos los pares de anclas. Siempre que sigamos la regla discutida anteriormente. Al mismo tiempo, podemos usar etiquetas específicas de anclaje más significativas (inferior/superior) fuera de la extensión. Como cuando se definen sobrecargas de operadores.

Una solución alternativa implicaría tener extensiones separadas para todos los tipos, lo cual no es tan malo ya que la cantidad de anclas es limitada. Pero yo era demasiado perezoso aquí y traté de crear una solución abstracta. De cualquier manera, este es un detalle de implementación que es interno de la biblioteca y se puede cambiar en el futuro sin interrumpir los cambios.

Deje un comentario si cree que un diseño diferente sería óptimo.

Más puntos de decisión arquitectónicos:

  • Las inserciones deben ser tipos separados, ya que se inicializan y se usan fuera de un ancla.
  • Los tipos anidados se utilizan para proteger el espacio de nombres y mantenerlo limpio, al mismo tiempo que se destaca el hecho de que una implementación de tipo Inset depende de la implementación específica de PairAnchor.

Nota: Agregar recuadros no es lo mismo que agregar constantes a una expresión.

Es posible que haya notado que agregamos un operador menos a la constante superior antes de devolverlo como parte de la interfaz EdgePair. De manera similar, las implementaciones de XAxisAnchorPair agregan un signo negativo al ancla final. Invertir las constantes significa que las inserciones funcionarán como inserciones en lugar de desplazar cada borde en la misma dirección.

La vista roja está restringida a 200 en la imagen uno y dos.

En la imagen de la izquierda, todos los anclajes de borde de la vista azul están configurados para ser iguales a los de la vista roja más 40. Lo que da como resultado que la vista azul tenga el mismo tamaño pero desplazada 40 a lo largo de ambos ejes. Si bien esto tiene sentido en términos de restricciones, no es una operación común en sí misma.

Establecer recuadros alrededor de una vista o agregar relleno alrededor de una vista es mucho más común. Por lo tanto, en el contexto de proporcionar una API para actores combinados tiene más sentido.

En la imagen de la derecha, configuramos la vista azul para que sea igual a la vista roja más un recuadro de 40 usando este DSL. Esto se logra mediante la inversión de constantes descrita anteriormente.

Paso 14: Amplíe View & LayoutGuide para inicializar anclajes compuestos.

Al igual que hicimos antes, ampliamos los tipos View y LayoutGuide con propiedades calculadas que inicializan LayoutBlocks cuando se les llama.

Paso 15: Sobrecargue los operadores +/- para permitir expresiones con inserciones de borde.

Para los anclajes de borde horizontal y borde vertical, queremos que el usuario pueda especificar recuadros. Para lograr esto, ampliamos el tipo UIEdgeInsets porque ya es familiar para la mayoría de los usuarios de este DSL.

La extensión permite la inicialización solo con inserciones superior/inferior o izquierda/derecha; el resto se establece de forma predeterminada en 0.

También necesitamos agregar una nueva propiedad a LayoutBlock para almacenar edgeInsets.

A continuación, sobrecargamos los operadores para las entradas: LayoutBlock con UIEdgeInsets .

Asignamos la instancia de UIEdgeInsets proporcionada por el usuario al tipo anidado relevante definido como parte de LayoutAnchorPair concreto .

Se ignorarán los parámetros adicionales o incorrectos que el usuario haya pasado como parte del tipo UIEdgeInset .

Paso 15: Sobrecargue los operadores de comparación para definir las relaciones de restricción.

El principio sigue siendo el mismo que antes. Sobrecargamos los operadores de relaciones para las entradas LayoutBlocks y LayoutAnchorPair .

Si un usuario proporciona un par de inserciones de borde, las usamos; de lo contrario, generamos un par de inserciones genéricas a partir de las constantes de LayoutBlocks . La estructura de inserción genérica es un contenedor, a diferencia de otras inserciones, no niega uno de los lados.

Paso 16: Diseño de dimensiónAnchorPair

Al igual que un ancla de una sola dimensión, un par de anclas de dimensión, anchoAnchor y altoAnchor , se pueden restringir a constantes. Por lo tanto, tenemos que proporcionar sobrecargas de operadores separadas para manejar este caso de uso.

  • Permita que el usuario del DSL fije tanto la altura como el ancho a la misma constante, creando un cuadrado.
  • Permita que el usuario del DSL fije el SizeAnchorPair a un tipo CGSize; tiene más sentido en la mayoría de los casos, ya que las vistas no son cuadrados.

Paso 17: Usar Generadores de resultados para manejar tipos [NSLayoutConstraint] y NSLayoutConstraint.

Los anclajes compuestos crean un problema interesante. El uso de estos anclajes en una expresión da como resultado una serie de restricciones. Esto podría resultar complicado para el usuario final del DSL.

Queremos proporcionar una forma de agrupar estas restricciones por lotes sin código repetitivo y activarlas de manera efectiva, de una vez en lugar de individualmente o en lotes separados.

Introducidos en Swift 5.4, los generadores de resultados (también conocidos como generadores de funciones) le permiten generar un resultado usando 'bloques de construcción' implícitamente a partir de una serie de componentes. De hecho, son el bloque de construcción subyacente detrás de la interfaz de usuario de Swift.

En este DSL, el resultado final es una matriz de objetos NSLayoutConstraint .

Proporcionamos funciones de compilación, que permiten que el generador de resultados resuelva restricciones individuales y matrices de restricciones en una matriz. Toda esta lógica está oculta para el usuario final del DSL.

La mayoría de estas funciones las copié directamente del ejemplo de propuesta del generador de resultados de evolución rápida . Agregué pruebas unitarias para asegurar que funcionen correctamente en este DSL.

Poniendo todo esto da como resultado lo siguiente:

Los generadores de resultados también nos permiten incluir un flujo de control adicional dentro del cierre, lo que no sería posible con una matriz.

Eso es todo, ¡gracias por leer! Este artículo tardó muchos días en escribirse, así que si aprendiste algo nuevo, ¡te agradecería un ⭐ en este repositorio!

Si tienes algún consejo para mí, no seas tímido: y comparte tu experiencia!

La versión final de este DSL incluye un ancla de cuatro dimensiones que está hecha de un par de tipos AnchorPair ...

Puedes encontrar todo el código aquí: