Teil 3: Schreiben einer Auto-Layout-DSL mit Swifts Operator Overloading und Result Builders ️

Nov 26 2022
Sie können sich auch diesen Artikel hier ansehen: Letztes Mal haben wir den Hauptteil der DSL gebaut, der es dem Endbenutzer ermöglicht, Einschränkungen auf arithmetische Weise auszudrücken. In diesem Artikel werden wir dies erweitern, um Folgendes einzuschließen: Zuerst erstellen wir einen Typ, der ein Ankerpaar enthält, das dem in Teil 1 definierten LayoutAnchor-Protokoll entspricht.

Sie können diesen Artikel auch hier ansehen:

Beim letzten Mal haben wir den Hauptteil der DSL erstellt, der es dem Endbenutzer ermöglicht, Einschränkungen auf arithmetische Weise auszudrücken.

In diesem Artikel werden wir dies erweitern, um Folgendes einzuschließen:

  • Kombinierte Anker – Größe, Mitte, horizontale Kanten, vertikale Kanten
  • Einschübe – Ermöglicht Benutzern das Festlegen von Einschüben auf kombinierten Kantenankern
  • Result Builder, um die Ausgabe verschiedener Anker zu verarbeiten, was eine einfache Bündelung und Aktivierung von Einschränkungen ermöglicht.

Zuerst erstellen wir einen Typ, der ein Ankerpaar enthält, das dem in Teil 1 definierten LayoutAnchor-Protokoll entspricht.

Hier habe ich überlegt, den LayoutBlock so zu ändern, dass er ein Array von Ankern enthält. Wenn wir dann eine Einschränkung aus zwei solchen Blöcken generieren, könnten wir die Anker zusammenzippen und über sie iterieren, die Anker aufeinander beschränken und die relevanten Konstanten/Multiplikatoren übergeben.

Es gibt 2 Nachteile:

  • Sogar einzelne Ausdrücke mit einfachen Ankern würden eine Reihe von Einschränkungen zurückgeben. Das erschwert das DSL aus Nutzersicht.
  • Ein Benutzer könnte versuchen, einen zusammengesetzten Anker (mit 2 oder 4 Ankern) mit einem einzelnen Anker zu verwenden. Wir können damit umgehen, indem wir die zusätzlichen Anker ignorieren. Der Compiler erzeugt keine Warnung. Diese Operationen und die daraus resultierenden Beschränkungen sind jedoch bedeutungslos. Dies hat das Potenzial, frustrierende Fehler in den Code des Endbenutzers einzuführen – etwas, das wir vermeiden möchten!!

Schritt 11: Erweitern Sie das LayoutAnchorPair-Protokoll.

Diese Erweiterung dient demselben Zweck wie die zuvor definierte LayoutAnchor-Protokollerweiterung – sie fungiert als Wrapper, der die Methoden des Basistyps aufruft und die resultierenden Einschränkungen zurückgibt.

Der Hauptunterschied besteht darin, dass jede Methode ein Array von Einschränkungen zurückgibt, das die Einschränkungen der beiden LayoutAnchor-Typen kombiniert.

Da die Anker, die wir an LayoutAnchorPair übergeben, auf LayoutAnchor-Typen beschränkt sind, können wir problemlos eine Standardimplementierung bereitstellen.

Außerdem verwenden diese Methoden anstelle von Konstanten ein EdgeInsetPair, das die Möglichkeit bietet, unterschiedliche Konstanten für jede der Einschränkungen bereitzustellen.

Jede Methode ordnet Konstante1 zu, um Anker1 und Konstante2 auf Anker2 einzuschränken.

Schritt 13: Erstellen Sie konkrete LayoutAnchor-Typen.

In Teil 1 und 2 mussten wir keine konkreten LayoutAnchor-Typen erstellen, da wir lediglich die Standard-NSLayoutAnchors an das LayoutAnchor-Protokoll angepasst haben. Hier müssen wir jedoch unsere eigenen Anker bereitstellen, die dem AnchorPair-Protokoll entsprechen.

Eine Erinnerung an zuvor verwendete Typealiases:

Wir definieren einen verschachtelten Typ, der das EdgeInsetPair-Protokoll erfüllt. Dies erfüllt die AnchorPair-assoziierte Typanforderung – Einfügungen. Der Betontyp, der diesem Protokoll entspricht, wird während des Vorgangs zum Überladen verwendet, um Einschübe zu setzen.

Wir verwenden berechnete Eigenschaften, um dem LayoutAnchorPair- und dem EdgeInsetPair-Protokoll zu entsprechen. Diese berechneten Eigenschaften geben die internen Eigenschaften von LayoutAnchorPair und EdgeInsetPair zurück.

Hier ist darauf zu achten, dass die vom Einfügungstyp gelieferten Konstanten mit den auf diesem Ankerpaar definierten Ankern übereinstimmen. Insbesondere im Zusammenhang mit der im letzten Schritt definierten Erweiterung, in der Konstante1 verwendet wird, um Anker1 einzuschränken .

Dieses „generische“ Protokoll ermöglicht es uns, eine Protokollerweiterung zu definieren, die für alle Ankerpaare gültig ist. Vorausgesetzt, wir befolgen die oben diskutierte Regel. Gleichzeitig können wir aussagekräftigere ankerspezifische Labels – unten/oben – außerhalb der Erweiterung verwenden. Beispielsweise beim Definieren von Operatorüberladungen.

Eine alternative Lösung würde separate Erweiterungen für alle Typen beinhalten – was nicht so schlimm ist, da die Anzahl der Anker begrenzt ist. Aber ich war hier zu faul und habe versucht, eine abstrakte Lösung zu schaffen. In jedem Fall handelt es sich um ein bibliotheksinternes Implementierungsdetail, das in Zukunft ohne Breaking Changes geändert werden kann.

Bitte hinterlassen Sie einen Kommentar, wenn Sie der Meinung sind, dass ein anderes Design optimal wäre.

Weitere architektonische Entscheidungspunkte:

  • Einsätze müssen separate Typen sein, da sie außerhalb eines Ankers initialisiert und verwendet werden.
  • Verschachtelte Typen werden verwendet, um Namespace zu schützen und sauber zu halten, während gleichzeitig die Tatsache hervorgehoben wird, dass eine Inset-Typimplementierung von der spezifischen PairAnchor-Implementierung abhängt.

Hinweis: Das Hinzufügen von Einschüben ist nicht dasselbe wie das Hinzufügen von Konstanten zu einem Ausdruck.

Sie haben vielleicht bemerkt, dass wir der obersten Konstante einen Minus -Operator hinzugefügt haben, bevor wir sie als Teil der EdgePair-Schnittstelle zurückgeben. In ähnlicher Weise fügen XAxisAnchorPair- Implementierungen dem abschließenden Anker ein Minus hinzu. Das Invertieren der Konstanten bedeutet, dass Einschübe als Einschübe funktionieren, anstatt jede Kante in die gleiche Richtung zu verschieben.

Die rote Ansicht ist in Bild eins und zwei auf 200 beschränkt.

Im linken Bild sind alle Kantenanker der blauen Ansicht gleich denen der roten Ansicht plus 40. Dies führt dazu, dass die blaue Ansicht dieselbe Größe hat, aber um 40 entlang beider Achsen verschoben ist. Obwohl dies in Bezug auf Einschränkungen sinnvoll ist, ist dies an sich keine übliche Operation.

Das Setzen von Einschüben um eine Ansicht oder das Hinzufügen von Polsterung um eine Ansicht herum ist viel üblicher. Daher ist es im Kontext der Bereitstellung einer API für kombinierte Akteure sinnvoller.

Im rechten Bild setzen wir mit dieser DSL die blaue Ansicht auf die rote Ansicht plus einen Einschub von 40. Dies wird durch die oben beschriebene Inversion von Konstanten erreicht.

Schritt 14: Erweitern Sie den View & LayoutGuide, um zusammengesetzte Anker zu initialisieren.

Genau wie zuvor erweitern wir die View- und LayoutGuide -Typen mit berechneten Eigenschaften, die LayoutBlocks initialisieren, wenn sie aufgerufen werden.

Schritt 15: Überladen Sie die +/- Operatoren, um Ausdrücke mit Randeinschüben zuzulassen.

Für horizontale Rand- und vertikale Randanker möchten wir, dass der Benutzer Einschübe angeben kann. Um dies zu erreichen, erweitern wir den Typ UIEdgeInsets , da er den meisten Benutzern dieser DSL bereits bekannt ist.

Die Erweiterung ermöglicht die Initialisierung nur mit oberen/unteren oder linken/rechten Einsätzen – der Rest ist standardmäßig auf 0 eingestellt.

Außerdem müssen wir LayoutBlock eine neue Eigenschaft hinzufügen, um edgeInsets zu speichern.

Als nächstes überladen wir Operatoren für Eingaben: LayoutBlock mit UIEdgeInsets .

Wir ordnen die vom Benutzer bereitgestellte Instanz von UIEdgeInsets dem relevanten verschachtelten Typ zu, der als Teil von konkretem LayoutAnchorPair definiert ist .

Zusätzliche oder falsche Parameter, die vom Benutzer als Teil des UIEdgeInset- Typs übergeben werden, werden ignoriert.

Schritt 15: Vergleichsoperatoren überladen, um Beschränkungsbeziehungen zu definieren.

Das Prinzip bleibt das gleiche wie bisher. Wir überladen Beziehungsoperatoren für LayoutBlocks- und LayoutAnchorPair- Eingaben.

Wenn ein Benutzer ein Paar Randeinschübe bereitstellt, verwenden wir sie, andernfalls generieren wir ein Paar generischer Einschübe aus LayoutBlocks- Konstanten. Die generische Inset-Struktur ist ein Wrapper, im Gegensatz zu anderen Insets negiert sie keine der Seiten.

Schritt 16: Dimension LayoutAnchorPair

Genau wie ein einzelner Dimensionsanker kann ein Dimensionsankerpaar – widthAnchor und heightAnchor – auf Konstanten beschränkt werden. Daher müssen wir separate Operatorüberladungen bereitstellen, um diesen Anwendungsfall zu behandeln.

  • Ermöglichen Sie dem Benutzer des DSL, sowohl Höhe als auch Breite auf dieselbe Konstante festzulegen – wodurch ein Quadrat entsteht.
  • Erlauben Sie dem Benutzer der DSL, das SizeAnchorPair auf einen CGSize-Typ festzulegen – ist in den meisten Fällen sinnvoller, da Ansichten keine Quadrate sind.

Schritt 17: Verwenden von Ergebnisgeneratoren zum Verarbeiten der Typen [NSLayoutConstraint] und NSLayoutConstraint.

Verbundanker schaffen ein interessantes Problem. Die Verwendung dieser Anker in einem Ausdruck führt zu einer Reihe von Einschränkungen. Dies könnte für den Endbenutzer des DSL chaotisch werden.

Wir möchten eine Möglichkeit bieten, diese Einschränkungen ohne Boilerplate-Code zusammenzufassen und effektiv zu aktivieren – auf einmal anstatt einzeln oder in separaten Batches.

Die in Swift 5.4 eingeführten Ergebnisgeneratoren (auch bekannt als Funktionsgeneratoren) ermöglichen es Ihnen, ein Ergebnis mithilfe von „Baublöcken“ implizit aus einer Reihe von Komponenten aufzubauen. Tatsächlich sind sie der zugrunde liegende Baustein hinter der Swift-Benutzeroberfläche.

In dieser DSL ist das Endergebnis ein Array von NSLayoutConstraint- Objekten.

Wir stellen Build-Funktionen bereit, die es dem Ergebnisgenerator ermöglichen, einzelne Einschränkungen und Arrays von Einschränkungen in einem Array aufzulösen. All diese Logik ist dem Endnutzer der DSL verborgen.

Die meisten dieser Funktionen habe ich direkt aus dem Vorschlagsbeispiel des Swift-Evolution- Ergebnisgenerators kopiert. Ich habe Komponententests hinzugefügt, um sicherzustellen, dass sie in dieser DSL korrekt funktionieren.

Setzt man das alles zusammen, ergibt sich folgendes:

Ergebnisgeneratoren ermöglichen es uns auch, einen zusätzlichen Kontrollfluss in den Abschluss einzufügen, was mit einem Array nicht möglich wäre.

Das ist es, danke fürs Lesen! Das Schreiben dieses Artikels hat viele Tage gedauert – wenn Sie also etwas Neues gelernt haben, würde ich mich über ein ⭐ für dieses Repo freuen!

Wenn du einen Rat für mich hast, sei nicht schüchtern: und teile deine Erfahrungen!

Die endgültige Version dieser DSL enthält einen vierdimensionalen Anker, der aus einem Paar von AnchorPair- Typen besteht…

Den ganzen Code findet ihr hier: