パート 3: Swift の演算子のオーバーロードと結果ビルダーを使用して自動レイアウト DSL を作成する ️

この記事はこちらからもご覧いただけます:
前回、DSL の主要部分を作成し、エンドユーザーが制約を算術的に表現できるようにしました。
この記事では、これを拡張して以下を含めます。
- 結合されたアンカー — size、center、 horizontalEdges、verticalEdges
- インセット — ユーザーが結合されたエッジ アンカーにインセットを設定できるようにする
- 異なるアンカーの出力を処理する Result Builder により、簡単な制約のバッチ処理とアクティブ化が可能になります。
最初に、パート 1 で定義された LayoutAnchor プロトコルに準拠するアンカーのペアを保持する型を作成します。
ここでは、アンカーの配列を保持するように LayoutBlock を変更することを検討しました。次に、そのような 2 つのブロックから制約を生成するときに、アンカーをまとめて圧縮し、それらを反復処理して、アンカーを互いに制約し、関連する定数/乗数を渡すことができます。
2 つの欠点があります。
- 基本的なアンカーを持つ単一の式でさえ、制約の配列を返します。これは、ユーザーの観点から DSL を複雑にします。
- ユーザーは、複合アンカー (2 つまたは 4 つのアンカー) を単一のアンカーで使用しようとする場合があります。これは、追加のアンカーを無視することで処理できます。コンパイラは警告を生成しません。ただし、これらの操作と結果として生じる制約は無意味になります。これにより、エンド ユーザーのコードにイライラするバグが発生する可能性があります。これは避けたいことです。
ステップ 11: LayoutAnchorPair プロトコルを拡張します。
この拡張機能は、以前に定義された LayoutAnchor プロトコル拡張機能と同じ目的を果たします — 基本型のメソッドを呼び出し、結果の制約を返すラッパーとして機能します。
ここでの主な違いは、各メソッドが 2 つの LayoutAnchor タイプの制約を組み合わせた制約の配列を返すことです。
LayoutAnchorPair に渡すアンカーは LayoutAnchor 型に制限されているため、デフォルトの実装を簡単に提供できます。
さらに、定数の代わりに、これらのメソッドは EdgeInsetPair を受け取り、各制約に異なる定数を提供する機能を提供します。
各メソッドは、定数 1 をマップして、アンカー 1 と定数 2 をアンカー 2 に制約します。
ステップ 13: 具体的な LayoutAnchor タイプを作成します。
パート 1 と 2 では、デフォルトの NSLayoutAnchor を LayoutAnchor プロトコルに準拠させただけなので、具体的な LayoutAnchor タイプを作成する必要はありませんでした。ただし、ここでは、AnchorPair プロトコルに準拠する独自のアンカーを提供する必要があります。
以前に使用された typealiases のリマインダー:
EdgeInsetPair プロトコルを満たすネストされた型を定義します。これは、AnchorPair に関連付けられた型の要件 — Insets を満たします。このプロトコルに準拠する具象型は、インセットを設定する操作のオーバーロード中に使用されます。
計算されたプロパティを使用して、LayoutAnchorPair および EdgeInsetPair プロトコルに準拠します。これらの計算されたプロパティは、LayoutAnchorPair と EdgeInsetPair の内部プロパティを返します。
ここでは、インセット タイプによって提供される定数が、このアンカー ペアで定義されたアンカーと一致することを確認することが重要です。具体的には、最後のステップで定義された拡張機能のコンテキストで、定数 1 を使用してアンカー 1 を制約します。
この「一般的な」プロトコルにより、すべてのアンカー ペアに対して有効な 1 つのプロトコル拡張を定義できます。上記のルールに従うことを条件とします。同時に、より意味のあるアンカー固有のラベル (下部/上部) を拡張機能の外側で使用できます。演算子のオーバーロードを定義するときなど。
別の解決策として、すべてのタイプに個別の拡張機能を用意する必要があります。これは、アンカーの数が限られているため、それほど悪くはありません。しかし、私はここで怠惰になり、抽象的な解決策を作成しようとしました。いずれにせよ、これはライブラリの内部にある実装の詳細であり、変更を壊すことなく将来変更することができます。
異なるデザインが最適だと思われる場合は、コメントを残してください。
その他のアーキテクチャ上の決定事項:
- インセットは、アンカーの外部で初期化および使用されるため、別個のタイプである必要があります。
- ネストされた型は、名前空間を保護してクリーンに保つために使用されますが、Inset 型の実装が特定の PairAnchor 実装に依存するという事実も強調しています。
注: インセットを追加することは、式に定数を追加することと同じではありません。
EdgePair インターフェイスの一部として返す前に、一番上の定数にマイナス演算子を追加したことに気付いたかもしれません。同様に、XAxisAnchorPairの実装では、末尾のアンカーにマイナスが追加されます。定数を逆にするということは、各エッジを同じ方向にシフトするのではなく、インセットがインセットとして機能することを意味します。

左のイメージでは、青のビューのすべてのエッジ アンカーが、赤のビューのエッジ アンカーに 40 を加えた値に等しくなるように設定されています。これにより、青のビューは同じサイズになりますが、両方の軸に沿って 40 だけシフトされます。これは制約の観点からは理にかなっていますが、これ自体は一般的な操作ではありません。
ビューの周りにインセットを設定したり、ビューの周りにパディングを追加したりすることは、はるかに一般的です。したがって、結合されたアクターに API を提供するというコンテキストでは、より理にかなっています。
右の画像では、この DSL を使用して、青のビューを赤のビューに 40 のインセットを加えたものと等しくなるように設定しています。これは、上記の定数の反転によって達成されます。
ステップ 14: View & LayoutGuide を拡張して複合アンカーを初期化します。
前と同じように、呼び出されたときにLayoutBlocksを初期化する計算されたプロパティでViewおよびLayoutGuide型を拡張します。
ステップ 15: +/- 演算子をオーバーロードして、エッジ インセットを含む式を許可します。
水平エッジと垂直エッジのアンカーについては、ユーザーがインセットを指定できるようにしたいと考えています。これを実現するために、UIEdgeInsets型を拡張します。これは、この DSL のほとんどのユーザーに既になじみがあるためです。
この拡張機能により、上下または左右のインセットのみで初期化できます。残りはデフォルトで 0 です。
また、 edgeInsetsを格納するために、LayoutBlock に新しいプロパティを追加する必要があります。
次に、入力の演算子をオーバーロードします: LayoutBlock with UIEdgeInsets。
ユーザーによって提供されたUIEdgeInsetsのインスタンスを、具体的なLayoutAnchorPairの一部として定義された関連するネストされた型にマップします。
UIEdgeInset型の一部としてユーザーによって渡された余分なパラメーターまたは正しくないパラメーターは無視されます。
ステップ 15: 比較演算子をオーバーロードして、制約関係を定義します。
原則は以前と同じです。LayoutBlocksおよびLayoutAnchorPair入力の関係演算子をオーバーロードします。
ユーザーがエッジ インセットのペアを提供する場合 — それらを使用します。それ以外の場合は、LayoutBlocks定数からジェネリック インセットのペアを生成します。一般的な inset 構造体はラッパーであり、他の inset とは異なり、どちらか一方を否定しません。
ステップ 16: LayoutAnchorPair のディメンション
1 つのディメンション アンカーと同様に、1 組のディメンション アンカー ( widthAnchorとheightAnchor ) を定数に制限できます。したがって、このユース ケースを処理するには、別の演算子のオーバーロードを提供する必要があります。
- DSL のユーザーが高さと幅の両方を同じ定数に固定できるようにします — 正方形を作成します。
- DSL のユーザーがSizeAnchorPairを CGSize 型に修正できるようにします — ほとんどの場合、ビューは正方形ではないため、より理にかなっています。
ステップ 17: [NSLayoutConstraint] および NSLayoutConstraint タイプを処理するために結果ビルダを使用する。
複合アンカーは興味深い問題を引き起こします。これらのアンカーを式で使用すると、制約の配列が生成されます。これは、DSL のエンド ユーザーにとって厄介なことになるかもしれません。
ボイラープレート コードを使用せずにこれらの制約をまとめてバッチ処理し、効果的に有効化する方法を提供したいと考えています。
Swift 5.4 で導入された結果ビルダー (関数ビルダーとも呼ばれます) を使用すると、一連のコンポーネントから暗黙的に「ビルド ブロック」を使用して結果を構築できます。実際、これらは Swift UI の背後にある基礎となるビルディング ブロックです。
この DSL では、最終結果はNSLayoutConstraintオブジェクトの配列です。
結果ビルダーが個々の制約と制約の配列を 1 つの配列に解決できるようにするビルド関数を提供します。このロジックはすべて、DSL のエンド ユーザーには隠されています。
これらの関数のほとんどは、swift-evolution の結果ビルダーの提案例から直接コピーしました。この DSL で正しく機能することを確認するために、単体テストを追加しました。
これをすべて入れると、次のようになります。
結果ビルダーを使用すると、クロージャー内に追加の制御フローを含めることもできますが、これは配列では不可能です。
以上です、読んでいただきありがとうございます!この記事を書くのに何日もかかったので、何か新しいことを学んだ場合は、このレポに ⭐ を付けていただければ幸いです。
私に何かアドバイスがあれば、恥ずかしがらずにあなたの経験を共有してください!
この DSL の最終バージョンには、AnchorPair型のペアから作成される 4 次元のアンカーが含まれています…
ここですべてのコードを見つけることができます: