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
- 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. - Check the component’s generic parameters. For this design,
Tiledepends onContent; its type does not include the concrete delegate type. - Check actor isolation. Keep UI callbacks and the illustrated observable model on the main actor. Calls from another isolation context must respect that boundary.
- Check the event location. Send selection from the button action, not while computing
bodyor initializing a view. - Check model ownership. Preserve
@StateObjectin 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
- Generics — The Swift Programming Language
- Protocols — The Swift Programming Language
- Declarations — The Swift Programming Language
- Support recursive constraints on associated types — Swift Evolution
- Introduce existential any — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Constrained Existential Types — Swift Evolution
- View — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Button — Apple Developer Documentation