共有インターフェースを強制しながら異なるSwiftUIシェイプを1つのセットに格納するには、プロトコル制約付きイニシャライザを持つ具象Hashableラッパーを使用します。このチュートリアルでは、明示的な重複ルールと呼び出し可能なシェイプ操作を持つ小さなMacシェイプパレットを構築します。

タスクは具体的です:合成ディスクおよびタイルコンポーネントを収集し、無関係な値をコンパイル時に拒否し、重複を削除し、残りのコンポーネントをSwiftUIウィンドウに表示します。コレクションは、SwiftUIのShapeを洗練させた共有プロトコルを通じて複数のコンポーネントタイプを受け入れます。

なぜプロトコル値のセットではこの契約を満たせないのか?

SwiftのSetは、その要素型がHashableに準拠することを要求します。存在型内のすべての値がHashableに準拠することを宣言しても、その存在型自体に準拠が与えられるわけではありません。したがって、Set<any PaletteShape>がPaletteShapeがHashableを継承しているだけで有効になることはありません。Appleはコレクションの要素制約を文書化しており、Swift Evolutionは存在型コンテナとその基盤となる準拠値の区別を説明しています。Set、暗黙的に開かれる存在型。

any PaletteShapeのような存在型は、異なる準拠型を保持できます。難点は等価性です:継承されたEquatable要件は同じSelf型の2つの値を比較しますが、2つの存在型コンテナは異なる具体的な型を保持している可能性があります。Swiftの存在型値に対する拡張サポートは、それらのコンテナに対する普遍的な等価性操作を提供しません。すべてのプロトコルに対する存在型の解放。

これらの代替案は問題の異なる部分を解決します:

表現 混在する具体的型? 共有インターフェースの強制? 適切なセット要素?
[any PaletteShape] はい はい 配列はセットの一意性を提供しない
Set<S> with S: PaletteShape 選択された1つのS はい はい、その単一の具体的型に対して
Set<AnyHashable> はい シェイプ制約なし はい
以下のSet<AnyPaletteShape> はい、ラッパー内で はい、構築時 はい

AnyHashableは消去された等価性とハッシュを提供しますが、そのイニシャライザは一般的にハッシュ可能な値を受け入れます。したがって、それだけを含むセットは文字列や整数を受け入れる可能性があります。以下のカスタムラッパーは、アプリケーションのシェイププロトコルへのエントリを制限し、消去されたハッシュ可能な値とともにシェイプインターフェースを保持します。AnyHashable。

何を重複シェイプとみなすべきか?

ラッパーを実装する前に等価性を決定します。このパレットでは、2つのコンポーネントが同じ具体的なコンポーネント型を持ち、格納値が等しい場合に重複とみなします。SampleラベルのディスクとSampleラベルのタイルは別々のエントリとして残ります。同じラベルを持つ2つのディスクは1つにまとめられます。

ラベルは各サンプルコンポーネントの唯一の格納プロパティであるため、合成された等価性とハッシュに含まれます。これは説明のためのモデル選択です:視覚的類似性だけでは等価性を定義しません。Appleは、格納プロパティがハッシュ可能な構造体に対する自動Hashable合成を文書化し、等しい値が同じコンポーネントをそれぞれのハッシュャーに供給することを要求しています。Hashable。

ラッパーはまた、等価性とハッシュの両方に型識別子を含みます。これにより、消去された比較に関する仮定に依存するのではなく、同じ型ルールが明示的になります。AnyHashableは、標準表現を使用して特定の値を異なる基盤型間で比較できますが、Appleはそれを無条件の具体的型識別子として定義していません。ObjectIdentifierはメタタイプをサポートし、この例ではジェネリック入力型を識別できます。AnyHashable、ObjectIdentifier。

完全なMacパレット例を構築する

XcodeでmacOS SwiftUIアプリプロジェクトを使用し、Swift 5.7以降のコンパイラを使用します。再現可能なベースラインとして、Swift 5言語モードとmacOS 13.0以降の展開ターゲットを選択します。展開ターゲットはこのチュートリアルのための保守的な選択であり、以下のすべてのAPIがmacOS 13を必要とするという主張ではありません。

コンパイラの下限が重要なのは、この例がany PaletteShapeを格納しており、その継承プロトコルに関連型とSelf要件が含まれるためです。SE-0309は、この拡張された存在型サポートがSwift 5.7で実装されたことを記録しています。この例は、より新しい一般化された存在型準拠機能を必要としません。すべてのプロトコルに対する存在型の解放。

生成されたアプリ宣言とコンテンツビューを、以下の完全なソースファイルで置き換えてください。アプリターゲットには@main宣言を1つだけ保持してください。この例には、コンポーネント定義、ラッパー、サンプル値、チェック、ビュー、アプリエントリーポイントが含まれます。研究環境ではSwiftを利用できないため、このコードは検査されましたが、ここでコンパイルまたは実行はされていません。

import SwiftUI

protocol PaletteShape: Shape, Hashable {
    var label: String { get }
}

struct Disk: PaletteShape {
    let label: String

    func path(in rect: CGRect) -> Path {
        Path(ellipseIn: rect)
    }
}

struct Tile: PaletteShape {
    let label: String

    func path(in rect: CGRect) -> Path {
        var result = Path()
        result.addRect(rect)
        return result
    }
}

struct AnyPaletteShape: Hashable {
    private let base: any PaletteShape
    private let typeID: ObjectIdentifier
    private let key: AnyHashable

    init<S: PaletteShape>(_ value: S) {
        base = value
        typeID = ObjectIdentifier(S.self)
        key = AnyHashable(value)
    }

    var label: String { base.label }

    func path(in rect: CGRect) -> Path {
        base.path(in: rect)
    }

    static func == (
        lhs: AnyPaletteShape,
        rhs: AnyPaletteShape
    ) -> Bool {
        lhs.typeID == rhs.typeID && lhs.key == rhs.key
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(typeID)
        hasher.combine(key)
    }
}

func makePalette() -> Set<AnyPaletteShape> {
    var result: Set<AnyPaletteShape> = [
        AnyPaletteShape(Disk(label: "Disk")),
        AnyPaletteShape(Tile(label: "Tile"))
    ]

    let duplicate = result.insert(
        AnyPaletteShape(Disk(label: "Disk"))
    )
    precondition(!duplicate.inserted)
    precondition(result.count == 2)
    precondition(result.contains(
        AnyPaletteShape(Tile(label: "Tile"))
    ))

    let sameLabel: Set<AnyPaletteShape> = [
        AnyPaletteShape(Disk(label: "Sample")),
        AnyPaletteShape(Tile(label: "Sample"))
    ]
    precondition(sameLabel.count == 2)

    return result
}

struct ContentView: View {
    private let shapes = makePalette()

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Shape Palette")
                .font(.title)

            ForEach(
                shapes.sorted { $0.label < $1.label },
                id: \.self
            ) { item in
                HStack(spacing: 16) {
                    GeometryReader { geometry in
                        item.path(in: CGRect(
                            origin: .zero,
                            size: geometry.size
                        ))
                        .fill(Color.blue)
                    }
                    .frame(width: 56, height: 56)

                    Text(item.label)
                }
            }
        }
        .padding(24)
        .frame(minWidth: 300, minHeight: 220)
    }
}

@main
struct ShapePaletteApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

PaletteShapeはSwiftUIの実際のShapeプロトコルを継承します。ディスクとタイルはpath(in:)を提供します。これはAppleによって文書化された描画要件です。それらの実装は、楕円と長方形を作成するために文書化されたPath操作を使用します。正方形の表示フレームにより、サンプル楕円が円形に見えます。Shape、Path。

制約付きイニシャライザがエントリールールです:init<S: PaletteShape>は共有プロトコルに準拠する型のみを受け入れます。格納プロパティはプライベートであるため、呼び出し側は保持されたシェイプやその等価性キーを独立して置き換えることはできません。StringがPaletteShapeに準拠しないため、AnyPaletteShape("Disk")はコンパイル時準拠エラーを生成するはずです。その意図的な拒否は実行可能ファイルから除外してください。

セットが1つの具体的な要素型を持つにもかかわらず、各ラッパーは異なる具体的なシェイプを保持できます。そのlabelとpath(in:)メンバーは、このパレットに必要な操作を転送します。ラッパー自体はHashableを宣言しますが、Shapeは宣言しません。

挿入、メンバーシップ、表示順序をどう検証するか?

デバッグビルドでアプリを実行します。含まれるチェックは3つの期待される結果を表現しています:2番目の等しいディスクを挿入するとinsertedがfalseとして報告され、メンバーシップチェックで等しいタイルが見つかり、ラベルが一致しても異なるコンポーネント型が生存します。Appleはinsert(_:)が挿入フラグと結果のメンバーの両方を返すことを文書化しています。Set。

期待されるウィンドウには、Diskに続いてTileの2行が青いグラフィックで含まれます。ソートは、セットに定義された要素順序がないため、別の表示操作です。結果の配列もForEachのランダムアクセスコレクション要件を満たします。id: \.selfはラッパーのハッシュ可能な値を行識別子として使用します。Set、ForEach。

この順序は、ラベルが異なるため、提供された値に対して十分です。将来的にコンポーネントがラベルを共有する場合、決定論的なセカンダリ順序フィールドを追加してください。行が編集可能になり、ラベルが変わってもアイデンティティを保持する必要がある場合、安定した識別子を導入し、その識別子がセット等価性に参加するかどうかを別途決定してください。

セット挿入が既存の等しいコンポーネントのペイロードを置き換えることを期待しないでください。置き換えが意図された操作の場合、Appleはupdate(with:)を提供します。この区別は、後でプレゼンテーションプロパティが等価性から除外されるモデルの場合に重要です:等しい値を挿入すると、外観を更新する代わりに既存のメンバーが保持される可能性があります。Set。

ラッパーの境界はどこにあるか?

HashableストレージはすべてのSwiftUI機能を保持しない

ラッパーは静的パレットのためにパス生成を転送します。基盤となるシェイプのアニメーション可能データ、インセット操作、またはViewから継承されるすべてのメンバーは転送しません。したがって、返されたパスを描画しても、アニメーションを保持するシェイプイレイザーは確立されません。SwiftUIのShapeもAnimatableとViewを継承します。転送された描画メソッドを1つ実装しても、その契約全体を再現することはできません。Shape。

別のAPIが具体的な消去済みシェイプを必要とする場合、SwiftUIはmacOS 13以降で利用可能なAnyShapeを提供します。その文書化された役割はシェイプの具体的な型を消去することであり、リストされている準拠にはHashableが含まれません。レンダリング境界には対応できますが、ここで構築した等価性ポリシーやプロトコル制約付きセット要素は提供しません。AnyShape。

新しいコンパイラサポートはセット要素制約を排除しない

Swift 5.7で実装されたSE-0352は、存在型の基盤型を適切なジェネリック呼び出しのために開くことを可能にします。その現在のテキストはSwift 6言語モードの動作も記録しています。値を開いて呼び出しを行っても、any PaletteShape自体がセットの要素パラメータに適したHashable型になるわけではありません。これらのコンパイラ世代でこのデザインを使用する場合、具体的なラッパーを保持してください。暗黙的に開かれる存在型。

モデルの拡張には等価性の決定が必要

サンプルは不変構造体を使用します。次元やその他の格納設定を追加する場合、それらを変更すると異なるセットメンバーが作成されるかどうかを決定してください。合成ハッシュには追加のハッシュ可能な格納プロパティが含まれます。カスタム等価性の場合、同じ本質的コンポーネントを同じ順序で正確にハッシュしてください。一致するハッシュ値だけでは等価性を確立しません。Hashable。

アプリケーションが永続的に固定されたコンポーネントバリアントのセットを持つ場合、ハッシュ可能な関連値を持つenumは別の合理的な設計です:サポートされるケースを一元化します。ラッパーは、中央のケースリストを拡張せずに、追加の準拠コンポーネント型が同じイニシャライザを通じて入力できる場合に有用です。その選択はアプリケーションの拡張ニーズに従います。

このMacパレットでは、実装は各要件に具体的な場所を与えます:プロトコルが許容されるシェイプを定義し、イニシャライザがエントリーを強制し、等価性が重複を定義し、パス転送がレンダリングを提供します。挿入時にラップすることで新しい不変準拠コンポーネントを追加し、重複の意味が変わるたびにチェックを拡張してください。

出典