Parte 3: Escrevendo um DSL de Layout Automático com Sobrecarga de Operador e Construtores de Resultados do Swift ️
Você também pode assistir a este artigo aqui:
Da última vez, construímos a parte principal do DSL, permitindo que o usuário final expresse as restrições de maneira aritmética.
Neste artigo, vamos expandir isso para incluir:
- Âncoras combinadas — tamanho, centro, horizontalEdges, verticalEdges
- Inserções — permite que os usuários definam inserções em âncoras de borda combinadas
- Construtor de resultados para lidar com a saída de diferentes âncoras, permitindo fácil ativação e agrupamento de restrições.
Primeiro, criamos um tipo para manter um par de âncoras em conformidade com o protocolo LayoutAnchor definido na Parte 1.
Aqui, considerei modificar o LayoutBlock para conter uma matriz de âncoras. Então, ao gerar uma restrição de 2 desses blocos, poderíamos fechar as âncoras juntas e iterar sobre elas, restringindo as âncoras umas às outras e passando as constantes/multiplicadores relevantes.
Existem 2 desvantagens:
- Mesmo expressões únicas com âncoras básicas retornariam uma série de restrições. Isso complica o DSL do ponto de vista do usuário.
- Um usuário pode tentar usar uma âncora composta (com 2 ou 4 âncoras) com uma âncora singular. Podemos lidar com isso ignorando as âncoras adicionais. O compilador não produzirá nenhum aviso. No entanto, essas operações e as restrições resultantes não terão sentido. Isso tem o potencial de introduzir bugs frustrantes no código do usuário final — algo que queremos evitar!!
Etapa 11: estenda o protocolo LayoutAnchorPair.
Essa extensão servirá ao mesmo propósito que a extensão do protocolo LayoutAnchor definida anteriormente — ela atua como um wrapper que chama os métodos do tipo base e retorna as restrições resultantes.
A principal diferença aqui é que cada método retorna um array de restrições, que combina as restrições dos 2 tipos de LayoutAnchor.
Como as âncoras que passamos para LayoutAnchorPair são restritas aos tipos LayoutAnchor, podemos fornecer uma implementação padrão facilmente.
Além disso, em vez de constantes, esses métodos recebem um EdgeInsetPair que oferecerá a capacidade de fornecer diferentes constantes para cada uma das restrições.
Cada método mapeia a constante1 para restringir a âncora1 e a constante2 na âncora2.
Passo 13: Crie tipos concretos de LayoutAnchor.
Nas Partes 1 e 2, não precisamos criar tipos concretos de LayoutAnchor, pois acabamos de tornar os NSLayoutAnchors padrão em conformidade com o protocolo LayoutAnchor. Aqui, porém, precisamos fornecer nossas próprias âncoras que estejam em conformidade com o protocolo AnchorPair.
Um lembrete dos typealiases usados anteriormente:
Definimos um tipo aninhado que satisfaz o protocolo EdgeInsetPair. Isso satisfaz o requisito de tipo associado AnchorPair — Insets. O tipo de concreto aderente a este protocolo será utilizado durante a operação de sobrecarga para definir insets.
Usamos propriedades calculadas para estar em conformidade com o protocolo LayoutAnchorPair e EdgeInsetPair. Essas propriedades computadas retornam as propriedades internas de LayoutAnchorPair e EdgeInsetPair.
Aqui é importante garantir que as constantes fornecidas pelo tipo de inserção correspondam às âncoras definidas neste par de âncoras. Especificamente no contexto da extensão definida na última etapa, onde constant1 é usado para restringir a âncora1 .
Este protocolo “genérico” nos permite definir uma extensão de protocolo válida para todos os pares de âncoras. Desde que sigamos a regra discutida acima. Ao mesmo tempo, podemos usar rótulos específicos de âncora mais significativos — inferior/superior — fora da extensão. Como ao definir sobrecargas de operador.
Uma solução alternativa implicaria ter extensões separadas para todos os tipos — o que não é tão ruim, já que o número de âncoras é limitado. Mas eu estava com preguiça aqui e tentei criar uma solução abstrata. De qualquer forma, este é um detalhe de implementação que é interno à biblioteca e pode ser alterado no futuro sem interromper as alterações.
Por favor, deixe um comentário se você acredita que um design diferente seria ideal.
Mais pontos de decisão arquiteturais:
- Inserções devem ser tipos separados, pois são inicializados e usados fora de uma âncora.
- Os tipos aninhados são usados para proteger o namespace e mantê-lo limpo, além de destacar o fato de que uma implementação do tipo Inset depende da implementação específica do PairAnchor.
Nota: Adicionar inserções não é o mesmo que adicionar constantes a uma expressão.
Você deve ter notado que adicionamos um operador menos à constante superior antes de retorná-la como parte da interface EdgePair. Da mesma forma, as implementações de XAxisAnchorPair adicionam um sinal de menos à âncora à direita. Inverter as constantes significa que as inserções funcionarão como inserções, em vez de deslocar cada aresta na mesma direção.
Na imagem à esquerda, todas as âncoras de borda da visualização azul são definidas para serem iguais às da visualização vermelha mais 40. O que resulta na visualização azul com o mesmo tamanho, mas deslocado em 40 ao longo de ambos os eixos. Embora isso faça sentido em termos de restrições, não é uma operação comum em si.
Definir inserções em torno de uma exibição ou adicionar preenchimento em torno de uma exibição é muito mais comum. Portanto, no contexto de fornecer uma API para atores combinados, faz mais sentido.
Na imagem à direita, definimos a exibição azul igual à exibição vermelha mais uma inserção de 40 usando este DSL. Isso é obtido com a inversão das constantes descritas acima.
Etapa 14: estenda o View & LayoutGuide para inicializar as âncoras compostas.
Assim como fizemos antes, estendemos os tipos View e LayoutGuide com propriedades computadas que inicializam LayoutBlocks quando chamados.
Etapa 15: Sobrecarregue os operadores +/- para permitir expressões com inserções de borda.
Para âncoras de borda horizontal e vertical, queremos que o usuário seja capaz de especificar inserções. Para conseguir isso, estendemos o tipo UIEdgeInsets porque já é familiar para a maioria dos usuários deste DSL.
A extensão permite a inicialização apenas com inserções superior/inferior ou esquerda/direita — o restante padrão é 0.
Também precisamos adicionar uma nova propriedade a LayoutBlock para armazenar edgeInsets.
Em seguida, sobrecarregamos os operadores para entradas: LayoutBlock com UIEdgeInsets .
Mapeamos a instância de UIEdgeInsets fornecida pelo usuário para o tipo aninhado relevante definido como parte do LayoutAnchorPair concreto .
Parâmetros extras ou incorretos passados pelo usuário como parte do tipo UIEdgeInset serão ignorados.
Etapa 15: Operadores de comparação de sobrecarga para definir relações de restrição.
O princípio continua o mesmo de antes. Sobrecarregamos operadores de relações para entradas LayoutBlocks e LayoutAnchorPair .
Se um usuário fornecer um par de inserções de borda — nós as usamos, caso contrário, geramos um par de inserções genéricas a partir das constantes LayoutBlocks . A estrutura de inserção genérica é um wrapper, ao contrário de outras inserções, ela não nega um dos lados.
Passo 16: Dimensão LayoutAnchorPair
Assim como uma âncora de dimensão única, um par de âncoras de dimensão — widthAnchor e heightAnchor — pode ser restringido a constantes. Portanto, temos que fornecer sobrecargas de operador separadas para lidar com esse caso de uso.
- Permita que o usuário do DSL fixe a altura e a largura na mesma constante - criando um quadrado.
- Permitir que o usuário do DSL corrija o SizeAnchorPair para um tipo CGSize — faz mais sentido na maioria dos casos, pois as visualizações não são quadradas.
Etapa 17: Usando os Construtores de resultados para lidar com os tipos [NSLayoutConstraint] e NSLayoutConstraint.
Âncoras compostas criam um problema interessante. O uso dessas âncoras em uma expressão resulta em uma matriz de restrições. Isso pode ficar confuso para o usuário final do DSL.
Queremos fornecer uma maneira de agrupar essas restrições sem código clichê e ativá-las de forma eficaz - de uma só vez, em vez de individualmente ou em lotes separados.
Introduzidos no Swift 5.4, os construtores de resultados (também conhecidos como construtores de funções) permitem que você crie um resultado usando 'blocos de construção' implicitamente de uma série de componentes. Na verdade, eles são o bloco de construção subjacente por trás do Swift UI.
Nesta DSL, o resultado final é um array de objetos NSLayoutConstraint .
Fornecemos funções de compilação, que permitem que o construtor de resultados resolva restrições individuais e matrizes de restrições em uma matriz. Toda essa lógica é escondida do usuário final do DSL.
A maioria dessas funções eu copiei diretamente do exemplo de proposta do construtor de resultados de evolução rápida . Adicionei testes de unidade para garantir que funcionem corretamente neste DSL.
Colocando tudo isso resulta no seguinte:
Os construtores de resultados também nos permitem incluir fluxo de controle adicional dentro do encerramento, o que não seria possível com um array.
É isso, obrigado por ler! Este artigo levou muitos dias para ser escrito - então, se você aprendeu algo novo, gostaria de receber um ⭐ neste repositório!
Se você tem algum conselho para mim, não seja tímido: e compartilhe sua experiência!
A versão final deste DSL inclui uma âncora de quatro dimensões que é feita de um par de tipos de AnchorPair …
Você pode encontrar todo o código aqui: