To store different SwiftUI shapes in one set while enforcing a shared interface, use a concrete Hashable wrapper with a protocol-constrained initializer. This tutorial builds a small Mac shape palette with explicit duplicate rules and callable shape operations.

The task is specific: collect synthetic disk and tile components, reject unrelated values at compile time, remove duplicates, and display the remaining components in a SwiftUI window. The collection accepts multiple component types through a shared protocol that refines SwiftUI’s Shape.

Why doesn’t a set of protocol values satisfy this contract?

Swift’s Set requires its element type to conform to Hashable. Declaring that every value inside an existential conforms to Hashable does not give that existential type its own conformance. Consequently, Set<any PaletteShape> does not become valid merely because PaletteShape inherits Hashable. Apple documents the collection’s element constraint, while Swift Evolution explains the distinction between an existential container and its underlying conforming value. Set, Implicitly Opened Existentials.

An existential such as any PaletteShape can hold different conforming types. The difficulty is equality: the inherited Equatable requirement compares two values of the same Self type, whereas two existential containers might hold different concrete types. Swift’s expanded support for existential values does not supply a universal equality operation for those containers. Unlock existentials for all protocols.

These alternatives solve different parts of the problem:

Representation Mixed concrete types? Shared interface enforced? Suitable set element?
[any PaletteShape] Yes Yes An array does not provide set uniqueness
Set<S> with S: PaletteShape One chosen S Yes Yes, for that single concrete type
Set<AnyHashable> Yes No shape constraint Yes
Set<AnyPaletteShape> below Yes, inside wrappers Yes, at construction Yes

AnyHashable supplies erased equality and hashing, but its initializer accepts hashable values generally. A set containing it alone could therefore accept a string or integer. The custom wrapper below restricts entry to the application’s shape protocol and retains the shape interface alongside the erased hashable value. AnyHashable.

What should count as a duplicate shape?

Decide equality before implementing the wrapper. For this palette, two components are duplicates when they have the same concrete component type and equal stored values. A disk labeled Sample and a tile labeled Sample remain separate entries. Two disks with the same label collapse into one.

The label is the only stored property in each sample component, so synthesized equality and hashing include it. This is an illustrative model choice: visual similarity alone does not define equality. Apple documents automatic Hashable synthesis for structures whose stored properties are hashable, and requires equal values to feed the same components into their hashers. Hashable.

The wrapper also includes a type identifier in both equality and hashing. This makes the same-type rule explicit rather than depending on assumptions about erased comparisons. AnyHashable can compare certain values across different underlying types using canonical representations; Apple does not define it as an unconditional concrete-type discriminator. ObjectIdentifier supports metatypes, which lets this example identify the generic input type. AnyHashable, ObjectIdentifier.

Build the complete Mac palette example

Use a macOS SwiftUI app project in Xcode with a Swift 5.7 or newer compiler. For a reproducible baseline, select Swift 5 language mode and a macOS deployment target of 13.0 or later. The deployment target is a conservative choice for this tutorial, not a claim that every API below requires macOS 13.

The compiler floor matters because the example stores any PaletteShape, whose inherited protocols include associated-type and Self requirements. SE-0309 records this expanded existential support as implemented in Swift 5.7. The example does not require a newer generalized existential-conformance feature. Unlock existentials for all protocols.

Replace the generated app declaration and content view with the following complete source file. Keep only one @main declaration in the app target. The example includes its component definitions, wrapper, sample values, checks, view, and app entry point. Swift is unavailable in the research environment, so this code was inspected but was not compiled or executed here.

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 inherits SwiftUI’s actual Shape protocol. The disk and tile provide path(in:), the drawing requirement documented by Apple. Their implementations use documented Path operations to create an ellipse and a rectangle. The square display frame makes the sample ellipse appear circular. Shape, Path.

The constrained initializer is the entry rule: init<S: PaletteShape> accepts only a type conforming to the shared protocol. The stored properties are private, so callers cannot replace the retained shape or its equality key independently. AnyPaletteShape("Disk") should produce a compile-time conformance error because String does not conform to PaletteShape; keep that deliberate rejection out of the runnable file.

Although the set has one concrete element type, each wrapper can retain a different concrete shape. Its label and path(in:) members forward the operations needed by this palette. The wrapper itself declares Hashable; it does not declare Shape.

How do you verify insertion, membership, and display order?

Run the app in a debug build. The included checks express three expected results: inserting a second equal disk reports inserted as false, membership finds an equal tile, and different component types survive even when their labels match. Apple documents insert(_:) as returning both an insertion flag and the resulting member. Set.

The expected window contains two rows, Disk followed by Tile, with blue graphics. Sorting is a separate display operation because a set has no defined element order. The resulting array also satisfies ForEach’s random-access collection requirement; id: \.self uses the wrapper’s hashable value as the row identifier. Set, ForEach.

This ordering is sufficient for the supplied values because their labels differ. If future components share labels, add a deterministic secondary ordering field. If rows become editable and must retain their identity while labels change, introduce a stable identifier and decide separately whether that identifier participates in set equality.

Do not expect set insertion to replace an existing equal component’s payload. When replacement is the intended operation, Apple provides update(with:). This distinction matters if a later model excludes presentation properties from equality: inserting an equal value can retain the existing member instead of refreshing its appearance. Set.

Where are the wrapper’s boundaries?

Hashable storage does not preserve every SwiftUI capability

The wrapper forwards path generation for a static palette. It does not forward the underlying shape’s animatable data, inset operations, or every member inherited from View. Drawing the returned path therefore does not establish an animation-preserving shape eraser. SwiftUI’s Shape also inherits Animatable and View; implementing one forwarded drawing method does not recreate that entire contract. Shape.

If another API needs a concrete erased shape, SwiftUI provides AnyShape, available on macOS 13 and later. Its documented role is to erase a shape’s concrete type, and its listed conformances do not include Hashable. It can address a rendering boundary, but it does not supply the equality policy or protocol-constrained set element built here. AnyShape.

New compiler support does not eliminate the set element constraint

SE-0352, implemented in Swift 5.7, allows an existential’s underlying type to be opened for suitable generic calls. Its current text also records Swift 6 language-mode behavior. Opening a value for a call does not make any PaletteShape itself a Hashable type suitable for the set’s element parameter. Keep the concrete wrapper when using this design across those compiler generations. Implicitly Opened Existentials.

Extending the model requires an equality decision

The sample uses immutable structures. When adding dimensions or other stored configuration, decide whether changing them creates a distinct set member. Synthesized hashing will include additional hashable stored properties. For custom equality, hash precisely the same essential components, in the same order; matching hash values alone never establish equality. Hashable.

If the application has a permanently fixed set of component variants, an enum with hashable associated values is another reasonable design: it centralizes the supported cases. The wrapper is useful when additional conforming component types should enter through the same initializer without expanding a central case list. That choice follows from the application’s extension needs.

For this Mac palette, the implementation gives each requirement a concrete place: the protocol defines accepted shapes, the initializer enforces entry, equality defines duplicates, and path forwarding supplies rendering. Add new immutable conforming components by wrapping them at insertion, then extend the checks whenever the meaning of a duplicate changes.

Sources