Część 3: Pisanie Auto Layout DSL za pomocą Swift's Operator Overloading i Result Builders ️

Możesz także obejrzeć ten artykuł tutaj:
Ostatnim razem zbudowaliśmy główną część DSL, umożliwiając użytkownikowi końcowemu wyrażanie ograniczeń w sposób arytmetyczny.
W tym artykule rozszerzymy to o:
- Łączone kotwice — rozmiar, środek, poziomeKrawędzie, pionoweKrawędzie
- Wstawki — umożliwiają użytkownikom ustawianie wpustów na połączonych kotwach krawędziowych
- Result Builder do obsługi danych wyjściowych różnych kotwic, umożliwiając łatwe grupowanie ograniczeń i aktywację.
Najpierw tworzymy typ do przechowywania pary kotwic zgodnych z protokołem LayoutAnchor zdefiniowanym w części 1.
Tutaj rozważałem zmodyfikowanie LayoutBlock, aby pomieścić tablicę kotwic. Następnie, generując ograniczenie z 2 takich bloków, moglibyśmy spakować kotwice i iterować po nich, ograniczając kotwice do siebie i przekazując odpowiednie stałe/mnożniki.
Są 2 wady:
- Nawet pojedyncze wyrażenia z podstawowymi zakotwiczeniami zwróciłyby tablicę ograniczeń. To komplikuje DSL z punktu widzenia użytkowników.
- Użytkownik może spróbować użyć kotwicy kompozytowej (z 2 lub 4 kotwicami) z pojedynczą kotwicą. Możemy sobie z tym poradzić, ignorując dodatkowe kotwice. Kompilator nie wyświetli żadnego ostrzeżenia. Jednak te operacje i wynikające z nich ograniczenia będą bez znaczenia. Może to potencjalnie wprowadzić frustrujące błędy do kodu użytkownika końcowego — coś, czego chcemy uniknąć!
Krok 11: Rozszerz protokół LayoutAnchorPair.
To rozszerzenie będzie służyć temu samemu celowi, co zdefiniowane wcześniej rozszerzenie protokołu LayoutAnchor — działa jako opakowanie, które wywołuje metody typu podstawowego i zwraca wynikowe ograniczenia.
Kluczową różnicą jest tutaj to, że każda metoda zwraca tablicę ograniczeń, która łączy ograniczenia 2 typów LayoutAnchor.
Ponieważ kotwice, które przekazujemy do LayoutAnchorPair, są ograniczone do typów LayoutAnchor, możemy łatwo zapewnić domyślną implementację.
Dodatkowo, zamiast stałych, te metody pobierają EdgeInsetPair, który oferuje możliwość dostarczania różnych stałych do każdego z ograniczeń.
Każda metoda odwzorowuje stałą1, aby ograniczyć kotwicę1 i stałą2 do kotwicy2.
Krok 13: Utwórz konkretne typy LayoutAnchor.
W części 1 i 2 nie musieliśmy tworzyć konkretnych typów LayoutAnchor, ponieważ właśnie dostosowaliśmy domyślne NSLayoutAnchor do protokołu LayoutAnchor. Tutaj jednak musimy zapewnić własne kotwice, które są zgodne z protokołem AnchorPair.
Przypomnienie używanych wcześniej typaliasów:
Definiujemy typ zagnieżdżony, który spełnia protokół EdgeInsetPair. Spełnia to wymaganie związane z typem AnchorPair — Insets. Beton zgodny z niniejszym protokołem będzie stosowany podczas eksploatacji przeciążeń do osadzania wypustek.
Używamy obliczonych właściwości, aby zachować zgodność z protokołami LayoutAnchorPair i EdgeInsetPair. Te obliczone właściwości zwracają wewnętrzne właściwości LayoutAnchorPair i EdgeInsetPair.
Tutaj ważne jest, aby upewnić się, że stałe dostarczane przez typ wstawki pasują do kotwic zdefiniowanych w tej parze kotwic. W szczególności w kontekście rozszerzenia zdefiniowanego w ostatnim kroku, gdzie stała1 jest używana do ograniczenia kotwicy1 .
Ten „ogólny” protokół pozwala nam zdefiniować jedno rozszerzenie protokołu, które jest ważne dla wszystkich par kotwic. Pod warunkiem, że zastosujemy się do zasady omówionej powyżej. Jednocześnie możemy używać bardziej znaczących etykiet specyficznych dla kotwicy — dół/góra — poza rozszerzeniem. Na przykład podczas definiowania przeciążeń operatora.
Alternatywne rozwiązanie wiązałoby się z oddzielnymi rozszerzeniami dla wszystkich typów — co nie jest takie złe, ponieważ liczba kotwic jest ograniczona. Ale byłem tu leniwy i próbowałem stworzyć abstrakcyjne rozwiązanie. Tak czy inaczej, jest to szczegół implementacji, który jest wewnętrzny dla biblioteki i można go zmienić w przyszłości bez przerywania zmian.
Zostaw komentarz, jeśli uważasz, że inny projekt byłby optymalny.
Więcej architektonicznych punktów decyzyjnych:
- Wstawki muszą być oddzielnymi typami, ponieważ są inicjowane i używane poza kotwicą.
- Typy zagnieżdżone służą do ochrony przestrzeni nazw i utrzymywania jej w czystości, jednocześnie podkreślając fakt, że implementacja typu Inset zależy od konkretnej implementacji PairAnchor.
Uwaga: Dodawanie wstawek to nie to samo, co dodawanie stałych do wyrażenia.
Być może zauważyłeś, że dodaliśmy operator minus do najwyższej stałej przed zwróceniem jej jako części interfejsu EdgePair. Podobnie implementacje XAxisAnchorPair dodają minus do końcowej kotwicy. Odwrócenie stałych oznacza, że wstawki będą działać jak wstawki, a nie przesuwać każdą krawędź w tym samym kierunku.

Na lewym obrazie wszystkie zakotwiczenia krawędzi widoku niebieskiego są ustawione tak, aby były równe zakotwiczeniu widoku czerwonego plus 40. W rezultacie widok niebieski ma ten sam rozmiar, ale jest przesunięty o 40 wzdłuż obu osi. Chociaż ma to sens pod względem ograniczeń, nie jest to powszechna operacja sama w sobie.
Ustawianie wstawek wokół widoku lub dodawanie wypełnienia wokół widoku jest znacznie bardziej powszechne. Dlatego w kontekście dostarczania API dla połączonych aktorów ma to większy sens.
Na prawym obrazie ustawiliśmy widok niebieski tak, aby był równy widokowi czerwonemu plus wstawka 40 za pomocą tego DSL. Osiąga się to poprzez odwrócenie opisanych powyżej stałych.
Krok 14: Rozszerz Przewodnik widoku i układu, aby zainicjować kotwice kompozytowe.
Tak jak poprzednio, rozszerzamy typy View i LayoutGuide o obliczone właściwości, które inicjują LayoutBlocks po wywołaniu.
Krok 15: Przeciąż operatory +/-, aby zezwolić na wyrażenia ze wstawkami krawędzi.
Dla zakotwiczeń krawędzi poziomej i pionowej chcemy, aby użytkownik mógł określić wypustki. Aby to osiągnąć, rozszerzamy typ UIEdgeInsets , ponieważ jest on już znany większości użytkowników tego DSL.
Rozszerzenie umożliwia inicjalizację za pomocą wstawek górnych/dolnych lub lewych/prawych — reszta ma domyślnie wartość 0.
Musimy również dodać nową właściwość do LayoutBlock , aby przechowywać edgeInsets.
Następnie przeciążamy operatory dla danych wejściowych: LayoutBlock z UIEdgeInsets .
Odwzorowujemy instancję UIEdgeInsets podaną przez użytkownika na odpowiedni typ zagnieżdżony zdefiniowany jako część konkretnej LayoutAnchorPair .
Dodatkowe lub nieprawidłowe parametry przekazane przez użytkownika jako część typu UIEdgeInset zostaną zignorowane.
Krok 15: Przeciążenie operatorów porównania w celu zdefiniowania relacji ograniczeń.
Zasada pozostaje taka sama jak poprzednio. Przeciążamy operatory relacji dla wejść LayoutBlocks i LayoutAnchorPair .
Jeśli użytkownik dostarczy parę wstawek krawędziowych — używamy ich, w przeciwnym razie generujemy parę ogólnych wstawek ze stałych LayoutBlocks . Ogólna struktura wstawek jest opakowaniem, w przeciwieństwie do innych wstawek, nie neguje jednej ze stron.
Krok 16: Układ wymiarówKotwicaPara
Podobnie jak w przypadku kotwicy jednowymiarowej, parę kotwic wymiarowych — widthAnchor i heightAnchor — można ograniczyć do stałych. Dlatego musimy zapewnić oddzielne przeciążenia operatorów, aby obsłużyć ten przypadek użycia.
- Pozwól użytkownikowi DSL ustalić tę samą stałą wysokość i szerokość — tworząc kwadrat.
- Zezwól użytkownikowi DSL na ustalenie typu SizeAnchorPair na typ CGSize — w większości przypadków ma to większy sens, ponieważ widoki nie są kwadratami.
Krok 17: Używanie konstruktorów wyników do obsługi typów [NSLayoutConstraint] i NSLayoutConstraint.
Kotwy kompozytowe stwarzają ciekawy problem. Użycie tych kotwic w wyrażeniu skutkuje tablicą ograniczeń. Może to być kłopotliwe dla użytkownika końcowego DSL.
Chcemy zapewnić sposób grupowania tych ograniczeń bez kodu standardowego i skutecznego ich aktywowania — za jednym razem, a nie pojedynczo lub w oddzielnych partiach.
Wprowadzone w Swift 5.4 konstruktory wyników (znane również jako konstruktory funkcji) umożliwiają budowanie wyniku przy użyciu „bloków konstrukcyjnych” niejawnie z serii komponentów. W rzeczywistości są one podstawowym elementem składowym Swift UI.
W tym DSL ostatecznym wynikiem jest tablica obiektów NSLayoutConstraint .
Zapewniamy funkcje kompilacji, które pozwalają konstruktorowi wyników rozwiązać poszczególne ograniczenia i tablice ograniczeń w jedną tablicę. Cała ta logika jest ukryta przed użytkownikiem końcowym DSL.
Większość z tych funkcji skopiowałem bezpośrednio z przykładowej propozycji konstruktora wyników Swift- Evolution. Dodałem testy jednostkowe, aby upewnić się, że działają poprawnie w tym DSL.
Umieszczenie tego wszystkiego w następujący sposób:
Konstruktorzy wyników pozwalają nam również uwzględnić dodatkowy przepływ sterowania w obrębie zamknięcia, co nie byłoby możliwe w przypadku tablicy.
To tyle, dziękuję za przeczytanie! Napisanie tego artykułu zajęło wiele dni — więc jeśli dowiedziałeś się czegoś nowego, byłbym wdzięczny za ⭐ w tym repozytorium!
Jeśli masz dla mnie jakąś radę, nie wstydź się: i podziel się swoim doświadczeniem!
Ostateczna wersja tego DSL zawiera czterowymiarową kotwicę, która składa się z pary typów AnchorPair …
Cały kod znajdziesz tutaj: