To let one delegate handle several SwiftUI component types, make the callback method generic and keep the component independent of the delegate’s concrete type. This guide builds a small macOS tile chooser that preserves the sender’s content type without a recursive Self requirement.

Choose the callback contract before choosing the syntax

The reader task is specific: build a reusable SwiftUI component whose content can vary, while one delegate receives selection callbacks from every supported content specialization. The demonstration uses two synthetic tiles—text and a circle—and displays the identifier of the selected tile.

Swift defines delegation as a protocol-based way for a class or structure to hand responsibilities to another instance. A delegate can receive an action together with its sender, as shown in the language guide’s delegation example. Protocols

For this component, the intended contract is: “For every content type conforming to View, the delegate can receive a tile containing that content.” Put the type parameter on the callback method. Swift’s generic functions select their concrete type arguments at each call, whereas an associated type is established by a protocol conformance. Those are different commitments. Generics

Requirement Suitable interface shape Consequence
One delegate handles every supported content type A generic callback method Each callback can have a different content type.
Each delegate handles one fixed content type An associated type Each conformance supplies one content type.
The callback needs only a tile identifier A callback taking the identifier The callback has no dependency on the rendering type.

This table is a design aid derived from those language rules. Choose the first row when preserving the typed sender is part of the API. Choose the third when a sender identifier supplies everything the receiver needs; carrying an entire component would then add an unnecessary dependency.

Remove the dependency on the delegate’s own type

A tightly coupled design can make the component generic over its delegate and then refer to that component as Component<Self> inside the delegate protocol. Here, Self means the eventual conforming delegate type. Associated types likewise belong to that conforming type. Declarations

That shape makes checking the callback depend on a component specialization that itself depends on the delegate. Whether a particular recursive declaration is accepted requires examining its complete constraints and compiler diagnostics. It would be inaccurate to say Swift rejects every recursive protocol relationship: SE-0157 added recursive associated-type constraints in Swift 4.1. Support recursive constraints on associated types

For the chosen task, the delegate’s concrete type does not need to appear in the component type at all. The design below gives Tile one generic parameter, Content. Its stored delegate is any TileDelegate, and the protocol requirement introduces its own method-level Content parameter. The callback preserves the sender’s content type without expressing it through the delegate’s Self.

The any spelling explicitly denotes an existential value: the stored value can hold an instance of a conforming delegate type. SE-0335 introduced that spelling in Swift 5.6. Using an existential for the delegate does not require erasing the tile’s content to AnyView. Introduce existential any

Build a complete macOS tile chooser

Use a macOS SwiftUI app target with a Swift 6 compiler, such as the compiler supplied with Xcode 16 or later. Set the macOS deployment target to 11.0 or later for this example’s @StateObject storage. Apple documents StateObject as available from macOS 11.0. StateObject

Replace the generated app declaration and starter view with this single source file. Ensure the target contains only one @main app declaration; Swift permits only one program entry-point approach. Declarations

import SwiftUI
import Combine

@MainActor
protocol TileDelegate: AnyObject {
    func didChoose<Content: View>(_ tile: Tile<Content>)
}

@MainActor
struct Tile<Content: View>: View {
    let id: String
    let delegate: any TileDelegate
    let content: Content

    init(
        id: String,
        delegate: any TileDelegate,
        @ViewBuilder content: () -> Content
    ) {
        self.id = id
        self.delegate = delegate
        self.content = content()
    }

    var body: some View {
        Button(action: {
            delegate.didChoose(self)
        }) {
            VStack(spacing: 8) {
                content
                    .frame(width: 100, height: 50)
                Text("Choose \(id)")
            }
            .padding(8)
        }
    }
}

@MainActor
final class SelectionModel: ObservableObject, TileDelegate {
    @Published private(set) var selection = "No tile selected"

    func didChoose<Content: View>(_ tile: Tile<Content>) {
        selection = "Selected: \(tile.id)"
    }
}

@MainActor
struct ChooserView: View {
    @StateObject private var model = SelectionModel()

    var body: some View {
        VStack(spacing: 16) {
            HStack(spacing: 16) {
                Tile(id: "text", delegate: model) {
                    Text("Sample")
                }

                Tile(id: "circle", delegate: model) {
                    Circle()
                }
            }

            Text(model.selection)
        }
        .padding(24)
        .frame(minWidth: 340, minHeight: 180)
    }
}

@main
@MainActor
struct DelegateDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ChooserView()
        }
    }
}

This is a complete app example, including imports, the delegate, component, model, and entry point. It was inspected but not compiled or executed here because the available environment has no Swift compiler or macOS SDK.

Apple’s Button API accepts an action and a view-producing label. In this example, clicking a tile invokes the delegate from the button action; evaluating body constructs the interface without sending a selection event. Button

Build and run the app on a Mac. Initially, the status should read No tile selected. Clicking Choose text should produce Selected: text; clicking Choose circle should produce Selected: circle. These are expected results from the example, not reported execution results.

Understand what the example preserves

The first tile has the type Tile<Text>, and the second has the type Tile<Circle>. Both use the same SelectionModel instance. Its generic callback meets the protocol requirement for either content type, while reading the common id property. This is the practical benefit of placing the generic parameter on the method.

The receiver can also access tile.content as Content. Inside the generic implementation, however, the declared constraint guarantees only the capabilities of View. Code that requires a text-specific operation would need a different contract or additional information; the callback cannot assume every content value is Text. This follows from Swift’s constraint-based generic model. Generics

The component implements body with some View, following Apple’s documented custom-view pattern. The current View declaration also carries main-actor isolation. The example explicitly marks its UI-facing protocol and types @MainActor so the callback and model update share that isolation. View

@StateObject supplies stable model storage for the lifetime of the containing view’s identity. Changes to the model’s published selection update the dependent status view. Moving model construction into an ordinary computed expression would no longer express that ownership through StateObject. StateObject

The example stores a required delegate strongly. Its model stores only the selection string, so the illustrated object graph has no reference back to a tile. If you later make the model retain senders, revisit ownership. Apple’s language guide demonstrates a class-bound delegate with weak storage when that relationship requires it. Protocols

When an associated type is the better contract

Suppose a receiver should accept only text tiles. An alternative protocol could declare associatedtype Content: View and require func didChoose(_ tile: Tile<Content>). That expresses one content type per conformance. Making Content a primary associated type provides convenient constraint syntax; it does not turn the requirement into a method that accepts every content type. SE-0346 introduced primary associated types in Swift 5.7. Lightweight same-type requirements for primary associated types

This alternative would be a different API version. You would also change the component’s delegate property and initializer parameter to the matching constrained existential, such as any FixedTileDelegate<Content>, and change each receiver to provide its fixed associated type and corresponding nongeneric callback. A text-only conformance could no longer serve the circle tile through that same conformance.

SE-0353 added constrained existential types in Swift 5.7, allowing an existential’s primary associated types to be bound to specified types. Binding one associated type still describes a fixed relationship. It does not supply a family of callback implementations for arbitrary content types. Constrained Existential Types

Keep compiler support separate from deployment support. SE-0353 explicitly notes runtime changes that are not backward deployable to existing OS releases. Before adopting the alternative on an older deployment target, check availability diagnostics in the actual target. The complete example above uses an ordinary delegate existential and does not require constrained existential storage. Constrained Existential Types

Diagnose a failing adaptation in a useful order

  1. Check the intended scope of the callback. If one delegate must handle text and circle tiles, its witness must retain the generic method signature. A method accepting only Tile<Text> does not fulfill that requirement.
  2. Check the component’s generic parameters. For this design, Tile depends on Content; its type does not include the concrete delegate type.
  3. Check actor isolation. Keep UI callbacks and the illustrated observable model on the main actor. Calls from another isolation context must respect that boundary.
  4. Check the event location. Send selection from the button action, not while computing body or initializing a view.
  5. Check model ownership. Preserve @StateObject in the containing view and avoid introducing a second model instance for the callback.

If an adaptation still fails, reduce it to one tile and one delegate implementation, then restore the second content type. That sequence distinguishes a protocol-witness problem from a view-composition problem. The example’s two different content types make a useful contract check: both must reach the same generic callback without casts or content erasure.

The essential design decision is where type variation belongs. For a delegate that handles every component specialization, put that variation on the callback method and keep the component’s type independent of the delegate’s concrete identity. The resulting API preserves a typed sender while keeping the macOS selection flow small enough to inspect and extend.

Sources