To construct a generic SwiftUI component, require its initializer in a protocol or inject a closure that returns it. This tutorial uses both approaches to display the same synthetic item in a macOS window, with explicit compiler and SDK boundaries.

What must a generic component promise?

Suppose a macOS app displays a small item card. You want a reusable panel to construct the card from an item, while allowing callers to choose the presentation. The central question is who owns construction: the component’s declared interface, or the caller supplying the component.

A generic constraint gives the implementation access to the capabilities stated by that constraint. Writing Component: View establishes that the component is a SwiftUI view; it does not establish that the component has an initializer named init(item:). Swift’s Generics chapter explains protocol constraints, while Apple’s View documentation describes the view’s required body property.

Consequently, this fragment is intentionally invalid:

// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
    Component(item: item)
}

Adding that initializer to one concrete component does not make it available through every generic parameter constrained only to View. The reusable API needs an explicit construction contract. Use this decision table before changing the component:

Construction requirement Suitable interface Reason
Every participating component accepts the same item Protocol requiring init(item:) Construction belongs to the component family’s interface.
Callers choose additional configuration or modifiers Factory closure returning the generic component Construction stays at the call site.
The caller already has a finished component Store the generic component value Construction has already happened.
Presentation switches among a finite set of layouts Compose the alternatives inside a SwiftUI view The surrounding API can retain a concrete view structure.

This table is a design aid derived from the documented language rules. The first two options are implemented together below so their behavior can be compared directly.

Build a complete macOS example

Use an Xcode macOS app target with SwiftUI and a macOS deployment target of 11 or later. The example is written for a Swift 6 compiler with the matching Apple SDK. Its explicit @MainActor annotations require Swift 5.5 or later, so the listing is not intended for earlier compilers. Apple documents WindowGroup as available from macOS 11 and as a container for an app’s view hierarchy. See WindowGroup.

Replace the generated app entry-point file with the following complete program. Ensure the target contains only one @main app declaration. The generated ContentView file can remain unused. No packages, external assets, or downloaded sample data are needed.

import SwiftUI

struct Item {
    let title: String
    let detail: String
}

@MainActor
protocol ItemComponent: View {
    init(item: Item)
}

@MainActor
struct ItemCard: ItemComponent {
    let item: Item

    init(item: Item) {
        self.item = item
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(item.title)
                .font(.headline)
            Text(item.detail)
        }
        .padding()
        .background(Color.gray.opacity(0.12))
        .cornerRadius(8)
    }
}

@MainActor
struct ContractPanel<Component: ItemComponent>: View {
    let item: Item

    init(item: Item, component: Component.Type) {
        self.item = item
    }

    var body: some View {
        Component(item: item)
    }
}

@MainActor
struct FactoryPanel<Component: View>: View {
    private let makeComponent: @MainActor () -> Component

    init(make: @escaping @MainActor () -> Component) {
        self.makeComponent = make
    }

    var body: some View {
        makeComponent()
    }
}

@MainActor
struct DemoView: View {
    private let item = Item(
        title: "Sample tile",
        detail: "Blue circle on a neutral background"
    )

    var body: some View {
        VStack(alignment: .leading, spacing: 18) {
            Text("Initializer contract")
            ContractPanel(item: item, component: ItemCard.self)

            Text("Injected factory")
            FactoryPanel {
                ItemCard(item: item)
                    .padding(.leading, 12)
            }

            Text("Alternative factory output")
            FactoryPanel {
                Text(item.title + ": " + item.detail)
            }
        }
        .padding(24)
        .frame(width: 420)
    }
}

@main
@MainActor
struct ComponentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DemoView()
        }
    }
}

Run the macOS app target. The intended result is one window containing two card presentations and a plain text presentation. The second card has additional leading padding supplied by its caller. These are synthetic examples of API behavior, not measured results. No Swift compiler is installed in the research environment, so this listing was inspected but was not compiled or executed here.

The main actor annotations make the UI construction boundary explicit. Apple’s current View declaration is annotated with @MainActor and @preconcurrency; its documentation also explains how isolation is inherited when conformance appears in a type’s original declaration. See View.

How does the initializer contract work?

ItemComponent refines View and adds exactly one construction requirement: init(item:). Swift supports initializer requirements directly in protocols. A conforming type supplies the implementation; the protocol states the signature without an initializer body. See Protocols: Initializer Requirements.

That requirement makes Component(item: item) valid inside ContractPanel. The component: Component.Type argument supplies a concrete type to generic inference: passing ItemCard.self determines that this panel’s component type is ItemCard. The panel does not need to store the metatype because its body constructs through the generic type parameter.

This contract is useful when every component should be constructible from the same input. For example, an app-owned family of compact and expanded item presentations could share the requirement. A caller can then change the selected component type without rewriting the panel’s construction expression.

The tradeoff appears when an implementation needs extra configuration. Requiring init(item:) means each conforming component must offer that construction path, even if its preferred initializer also accepts a caption or layout setting. If that extra information belongs to the caller, the factory approach below usually expresses the requirement more directly.

For class-based implementations, an initializer satisfying a protocol requirement must be required unless the class is final. A subclass that also overrides a matching superclass designated initializer may need both required and override. Structs such as ItemCard do not use required. These distinctions are documented in Protocols.

When should construction move into a factory?

FactoryPanel accepts a function of type @MainActor () -> Component. Its promise is simply that calling the function produces the selected component type. The closure can invoke an initializer, capture configuration, or return a view with modifiers applied.

The program demonstrates the practical difference: the factory can return a padded ItemCard, or it can return Text, without requiring either resulting type to adopt ItemComponent. Apple explains that view modifiers can wrap their input in another view, which is why letting the compiler infer the factory’s resulting type is useful here. See View.

The initializer marks the closure @escaping because the panel stores it for later invocation. Swift’s Closures chapter documents escaping closures and their ability to capture surrounding values. In this example, the closure captures the synthetic item used by the surrounding view.

Choose the factory’s inputs deliberately. A zero-argument factory means the caller captures everything needed for construction. If the panel itself selects an item, a parameterized factory can express that responsibility more clearly. The following is a replacement component definition to use alongside the existing Item; it is not a second complete app:

@MainActor
struct ItemFactoryPanel<Component: View>: View {
    let item: Item
    private let makeComponent: @MainActor (Item) -> Component

    init(
        item: Item,
        make: @escaping @MainActor (Item) -> Component
    ) {
        self.item = item
        self.makeComponent = make
    }

    var body: some View {
        makeComponent(item)
    }
}

With that definition, the panel supplies the item and the caller supplies the presentation. Keep factory work limited to producing the view value: the sample’s construction contract does not promise one-time execution, persistent storage, or caching.

Do factories need a result builder?

The factories in the complete program are ordinary closures. Each returns one expression, so no custom builder annotation is needed. If several child views are required, returning a single VStack is sufficient for this API; the stack contains the child views.

A result builder becomes useful when the factory parameter itself should accept a declarative sequence of expressions or conditional content. Swift Evolution records the standardized result-builder feature as implemented in Swift 5.4. See SE-0289: Result builders.

Preserve the SDK boundary when adopting builder syntax. Apple’s current documentation describes @ViewBuilder for view-producing parameters in Xcode 26 and earlier. For Xcode 27 and later, it describes type-agnostic construction and recommends the ContentBuilder alias to indicate that behavior. See ViewBuilder and ContentBuilder. The ordinary closure used here avoids requiring that newer alias.

Why don’t opaque or existential types replace the contract?

some View hides a specific underlying result type while preserving its identity. It works well for the body properties in this program. It does not add init(item:) to the View interface. A boxed value written as any View hides its underlying type differently and does not preserve the same static type identity. See Opaque and Boxed Protocol Types.

Do not turn this into a blanket claim that existential values can never be passed to generic functions. SE-0352 introduced implicitly opened existentials in Swift 5.7, allowing eligible calls to bind a generic parameter to an existential’s underlying type. It also documents restrictions, including results that embed the opened type in a generic wrapper where the result cannot be represented after erasure. See Implicitly Opened Existentials.

For this panel task, retaining the concrete component type through a generic parameter is sufficient. There is no need to introduce existential storage merely to let callers choose how to construct a view.

How can you diagnose a construction error?

Follow the expression that fails and inspect the contract available at that location:

  1. If Component(item:) fails, check that the generic constraint includes a protocol requiring that exact initializer signature.
  2. If the compiler cannot infer Component, supply a concrete metatype such as ItemCard.self, or a factory with a concrete return expression.
  3. If a component needs caller-specific configuration, move construction into the factory instead of adding unrelated requirements to the protocol.
  4. If storing a closure triggers an escaping-closure diagnostic, check the initializer’s @escaping annotation.
  5. If construction triggers an actor-isolation diagnostic, check that both the call site and the factory’s function type express the intended main actor boundary.
  6. If an existential causes a generic-result error, inspect the result-type restrictions in SE-0352 before assuming a compiler upgrade removes the limitation.

For a shared macOS component family, require the initializer when uniform construction is part of the interface. Use an injected factory when the caller should assemble and configure the presentation. Both paths make the missing construction promise explicit while preserving the concrete component type needed by the panel.

Sources