Parte 3: Scrivere un DSL con layout automatico con Operator Overloading e Result Builders di Swift ️
Puoi anche guardare questo articolo qui:
L'ultima volta abbiamo costruito la parte principale del DSL, consentendo all'utente finale di esprimere i vincoli in modo aritmetico.
In questo articolo espanderemo questo argomento per includere:
- Ancoraggi combinati: dimensioni, centro, bordi orizzontali, bordi verticali
- Inserti: consente agli utenti di impostare inserti su ancoraggi di bordo combinati
- Generatore di risultati per gestire l'output di diverse ancore, consentendo un facile raggruppamento e attivazione dei vincoli.
Per prima cosa creiamo un tipo per contenere una coppia di ancore conformi al protocollo LayoutAnchor definito nella Parte 1.
Qui ho pensato di modificare il LayoutBlock per contenere un array di ancoraggi. Quindi, quando generiamo un vincolo da 2 di questi blocchi, potremmo comprimere gli ancoraggi insieme e iterare su di essi, vincolando gli ancoraggi l'uno all'altro e passando le relative costanti/moltiplicatori.
Ci sono 2 svantaggi:
- Anche singole espressioni con ancore di base restituirebbero una serie di vincoli. Il complica il DSL dal punto di vista degli utenti.
- Un utente potrebbe provare a utilizzare un'ancora composita (con 2 o 4 ancore) con un'ancora singola. Possiamo gestirlo ignorando le ancore aggiuntive. Il compilatore non produrrà alcun avviso. Tuttavia, queste operazioni e i vincoli risultanti saranno privi di significato. Questo ha il potenziale per introdurre bug frustranti nel codice degli utenti finali, qualcosa che vogliamo evitare!!
Passaggio 11: estendere il protocollo LayoutAnchorPair.
Questa estensione avrà lo stesso scopo dell'estensione del protocollo LayoutAnchor definita in precedenza: funge da wrapper che chiama i metodi del tipo di base e restituisce i vincoli risultanti.
La differenza fondamentale qui è che ogni metodo restituisce una matrice di vincoli, che combina i vincoli dei 2 tipi LayoutAnchor.
Poiché gli ancoraggi che passiamo a LayoutAnchorPair sono vincolati ai tipi LayoutAnchor, possiamo fornire facilmente un'implementazione predefinita.
Inoltre, invece delle costanti, questi metodi accettano un EdgeInsetPair che offrirà la possibilità di fornire costanti diverse a ciascuno dei vincoli.
Ogni metodo mappa constant1 per vincolare anchor1 e constant2 su anchor2.
Passaggio 13: creare tipi LayoutAnchor concreti.
Nella parte 1 e 2 non è stato necessario creare tipi LayoutAnchor concreti poiché abbiamo appena reso i NSLayoutAnchors predefiniti conformi al protocollo LayoutAnchor. Qui, tuttavia, dobbiamo fornire le nostre ancore conformi al protocollo AnchorPair.
Un promemoria dei typealias usati in precedenza:
Definiamo un tipo annidato che soddisfa il protocollo EdgeInsetPair. Ciò soddisfa il requisito del tipo associato ad AnchorPair: gli inserti. Il tipo di calcestruzzo che aderisce a questo protocollo verrà utilizzato durante l'operazione di sovraccarico per impostare i riquadri.
Usiamo proprietà calcolate per conformarsi al protocollo LayoutAnchorPair e EdgeInsetPair. Queste proprietà calcolate restituiscono le proprietà interne di LayoutAnchorPair e EdgeInsetPair.
Qui è importante assicurarsi che le costanti fornite dal tipo di inserto corrispondano agli ancoraggi definiti su questa coppia di ancoraggi. In particolare nel contesto dell'estensione definita nell'ultimo passaggio, dove constant1 viene utilizzato per vincolare anchor1 .
Questo protocollo "generico" ci consente di definire un'estensione del protocollo valida per tutte le coppie di ancoraggi. A condizione che seguiamo la regola discussa sopra. Allo stesso tempo, possiamo utilizzare etichette specifiche di ancoraggio più significative - in basso/in alto - al di fuori dell'estensione. Ad esempio quando si definiscono gli overload degli operatori.
Una soluzione alternativa comporterebbe la presenza di estensioni separate per tutti i tipi, il che non è poi così male poiché il numero di ancore è limitato. Ma qui ero troppo pigro e ho cercato di creare una soluzione astratta. In ogni caso, questo è un dettaglio di implementazione che è interno alla libreria e può essere modificato in futuro senza interrompere le modifiche.
Si prega di lasciare un commento se si crede che un design diverso sarebbe ottimale.
Altri punti decisionali architettonici:
- Gli inserti devono essere di tipo separato poiché vengono inizializzati e utilizzati all'esterno di un'ancora.
- I tipi nidificati vengono utilizzati per proteggere lo spazio dei nomi e mantenerlo pulito, evidenziando anche il fatto che un'implementazione di tipo Inset dipende dall'implementazione specifica di PairAnchor.
Nota: l'aggiunta di inserti non equivale all'aggiunta di costanti a un'espressione.
Potresti aver notato che abbiamo aggiunto un operatore meno alla costante superiore prima di restituirlo come parte dell'interfaccia EdgePair. Allo stesso modo, le implementazioni di XAxisAnchorPair aggiungono un meno all'ancora finale. Invertire le costanti significa che gli inserti funzioneranno come inserti piuttosto che spostare ciascun bordo nella stessa stessa direzione.
Nell'immagine a sinistra, tutti gli ancoraggi del bordo della vista blu sono impostati per essere uguali a quelli della vista rossa più un 40. Il che fa sì che la vista blu sia della stessa dimensione ma spostata di 40 lungo entrambi gli assi. Sebbene ciò abbia senso in termini di vincoli, non si tratta di un'operazione comune in sé.
L'impostazione di rientri attorno a una vista o l'aggiunta di spaziatura attorno a una vista è molto più comune. Quindi nel contesto della fornitura di un'API per attori combinati ha più senso.
Nell'immagine a destra, impostiamo la visualizzazione blu in modo che sia uguale alla visualizzazione rossa più un riquadro di 40 utilizzando questo DSL. Ciò si ottiene con l'inversione delle costanti sopra descritte.
Passaggio 14: estendere View & LayoutGuide per inizializzare gli ancoraggi compositi.
Proprio come abbiamo fatto prima, estendiamo i tipi View e LayoutGuide con proprietà calcolate che inizializzano LayoutBlocks quando vengono chiamati.
Passaggio 15: sovraccaricare gli operatori +/- per consentire espressioni con bordi inseriti.
Per gli ancoraggi del bordo orizzontale e del bordo verticale vogliamo che l'utente sia in grado di specificare gli inserti. Per ottenere ciò, estendiamo il tipo UIEdgeInsets perché è già familiare alla maggior parte degli utenti di questo DSL.
L'estensione consente l'inizializzazione solo con gli inserti in alto/in basso o sinistra/destra — il resto è impostato su 0 per impostazione predefinita.
Abbiamo anche bisogno di aggiungere una nuova proprietà a LayoutBlock per memorizzare edgeInsets.
Quindi sovraccarichiamo gli operatori per gli input: LayoutBlock con UIEdgeInsets .
Mappiamo l'istanza di UIEdgeInsets fornita dall'utente al tipo nidificato pertinente definito come parte di LayoutAnchorPair concreto .
I parametri aggiuntivi o non corretti passati dall'utente come parte del tipo UIEdgeInset verranno ignorati.
Passo 15: Sovraccaricare gli operatori di confronto per definire le relazioni di vincolo.
Il principio rimane lo stesso di prima. Sovraccarichiamo gli operatori di relazione per gli input LayoutBlocks e LayoutAnchorPair .
Se un utente fornisce una coppia di margini interni, li usiamo, altrimenti generiamo una coppia di margini generici dalle costanti LayoutBlocks . La struttura generica dell'inset è un wrapper, a differenza di altri inset non nega uno dei lati.
Passo 16: Quota LayoutAncoraCoppia
Proprio come un'ancora di dimensione singola, una coppia di ancore di dimensione — widthAnchor e heightAnchor — può essere vincolata a costanti. Pertanto, dobbiamo fornire overload di operatori separati per gestire questo caso d'uso.
- Consenti all'utente del DSL di fissare sia l'altezza che la larghezza alla stessa costante, creando un quadrato.
- Consenti all'utente del DSL di correggere SizeAnchorPair su un tipo CGSize: ha più senso nella maggior parte dei casi poiché le viste non sono quadrati.
Passaggio 17: utilizzo dei generatori di risultati per gestire i tipi [NSLayoutConstraint] e NSLayoutConstraint.
Gli ancoraggi compositi creano un problema interessante. L'utilizzo di questi ancoraggi in un'espressione comporta una matrice di vincoli. Questo potrebbe diventare complicato per l'utente finale del DSL.
Vogliamo fornire un modo per raggruppare questi vincoli senza codice boilerplate e attivarli in modo efficace, in una volta sola piuttosto che individualmente o in batch separati.
Introdotti in Swift 5.4, i generatori di risultati (noti anche come generatori di funzioni) consentono di creare un risultato utilizzando "build block" implicitamente da una serie di componenti. In effetti, sono l'elemento costitutivo alla base dell'interfaccia utente di Swift.
In questo DSL il risultato finale è un array di oggetti NSLayoutConstraint .
Forniamo funzioni di compilazione, che consentono al generatore di risultati di risolvere singoli vincoli e array di vincoli in un unico array. Tutta questa logica è nascosta all'utente finale del DSL.
La maggior parte di queste funzioni l'ho copiata direttamente dall'esempio di proposta del generatore di risultati in rapida evoluzione . Ho aggiunto unit test per assicurarmi che funzionino correttamente in questo DSL.
Mettendo tutto questo si ottiene quanto segue:
I generatori di risultati ci consentono inoltre di includere un flusso di controllo aggiuntivo all'interno della chiusura, cosa che non sarebbe possibile con un array.
Questo è tutto, grazie per aver letto! Ci sono voluti molti giorni per scrivere questo articolo, quindi se hai imparato qualcosa di nuovo apprezzerei un ⭐ su questo repository!
Se hai qualche consiglio da darmi, non essere timido: e condividi la tua esperienza!
La versione finale di questo DSL include un'ancora quadridimensionale composta da una coppia di tipi AnchorPair ...
Trovi tutto il codice qui: