For a macOS window that displays heterogeneous SwiftUI components, keep each provider’s output strongly typed and erase it when the shared rendering interface needs one concrete view type. Use provider erasure earlier when the collection itself needs a uniform wrapper.

This article builds a small component gallery containing synthetic text and a blue swatch. The task is to store different provider implementations together and render them through one SwiftUI interface. The design decision is where to stop exposing the relationship between a provider and its output.

What does the provider contract need to preserve?

A provider with an associated output type says that each conforming type chooses its own view type. A concrete implementation can hide that choice behind some View, while generic consumers retain the relationship between the provider and its output. Swift’s opaque types preserve type identity; boxed protocol types allow different underlying types behind a shared interface. See Opaque and Boxed Protocol Types.

The following is a contract fragment, not a complete application:

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title).font(.headline)
    }
}

Here, TitleProvider.Content is inferred from the opaque return type of makeView(). The protocol requires an associated type, and the implementation supplies it through inference. SE-0244 explicitly documents this supported pattern and the restriction against declaring an opaque return type directly as a protocol requirement. Thus, replacing the requirement with func makeView() -> some View is not the supported contract described by that proposal. See Opaque Result Types.

An array of any ViewProvider solves heterogeneous storage. Swift 5.7’s SE-0309 permits existential use of protocols with associated types, while retaining restrictions on individual member operations. Reading an output through that existential can erase its associated type to its protocol upper bound—in this case, any View. See Unlock existentials for all protocols.

That gives you two separate decisions: how to store mixed providers, and what concrete type the renderer returns. Apple documents AnyView as a concrete, type-erased view conforming to View, making it an explicit option for the second decision. See AnyView.

Choose the boundary from the consumer’s requirements

Use this decision table before changing the protocol. These recommendations apply the documented type contracts to the gallery task; they are design guidance rather than measured performance results.

Consumer requirement Suitable design Where type information is hidden
Render one concrete provider through reusable code A generic consumer constrained to ViewProvider Keep the provider/output relationship available
Store different providers and retain their shared metadata [any ViewProvider], with a rendering helper returning AnyView Provider type at storage; output type at rendering
Expose one concrete provider wrapper to downstream code [AnyViewProvider] At wrapper construction
Require every implementation to return an already erased view A protocol requirement returning AnyView Inside each implementation
Display a fixed, small set of component alternatives An enum and a builder-based switch Alternatives remain explicit in the enum

For this gallery, choose existential storage plus output erasure. Providers retain their own output types, the collection exposes a stable string identifier, and one helper owns conversion to AnyView. Changing a component’s internal layout then requires no new erasure code in that component.

A generic collection parameter would serve a different task. An array whose element type is one generic provider type cannot automatically represent several unrelated provider types. Swift’s implicit existential opening works on an individual existential argument; it does not give a heterogeneous array one shared underlying conforming type. See Implicitly Opened Existentials.

Build a complete macOS component gallery

The example requires a Swift 5.7 or later compiler and a macOS SwiftUI application target. Set the sample’s deployment target to macOS 12 or later. In an Xcode macOS App project using SwiftUI, replace the generated app declaration with the complete program below, retaining exactly one @main declaration.

The explicit @MainActor annotations keep provider creation and rendering on the same actor. Apple’s current View declaration is main-actor isolated. Its documentation also explains that custom views supply their content through body. See View.

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title)
            .font(.headline)
    }
}

@MainActor
struct SwatchProvider: ViewProvider {
    let id: String

    func makeView() -> some View {
        RoundedRectangle(cornerRadius: 8)
            .fill(Color.blue)
            .frame(width: 96, height: 48)
    }
}

@MainActor
func erasedView<P: ViewProvider>(for provider: P) -> AnyView {
    AnyView(provider.makeView())
}

@MainActor
struct GalleryView: View {
    let providers: [any ViewProvider]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            ForEach(providers, id: \.id) { provider in
                erasedView(for: provider)
            }
        }
        .padding(24)
        .frame(minWidth: 280, minHeight: 180)
    }
}

@main
@MainActor
struct ProviderGalleryApp: App {
    private let providers: [any ViewProvider] = [
        TitleProvider(id: "title", title: "Component gallery"),
        SwatchProvider(id: "swatch")
    ]

    var body: some Scene {
        WindowGroup {
            GalleryView(providers: providers)
        }
    }
}

The intended result is one window showing Component gallery above a blue rounded rectangle. All inputs are local synthetic values. The example has been inspected but was not compiled here because the available environment has no Swift compiler or Apple SDK.

The helper’s result is always AnyView, regardless of the provider’s associated output type. Inside the helper, the provider is generic and its Content relationship remains available. Apple documents the initializer used here as accepting a generic value constrained to View and erasing that value. See AnyView.init(_:).

The collection uses id: \.id because the protocol fixes id to String; it does not require recovering another unknown associated type. Apple’s ForEach documentation permits either identifiable elements or an explicit identifier key path. Keep identifiers unique within this collection and stable for the logical components across updates. See ForEach.

Why can a generic helper work when a generic view wrapper fails?

The important property of erasedView is its concrete return type. Swift 5.7 introduced implicit opening of existential arguments, allowing the compiler to bind a generic parameter to an existential value’s underlying type for a call. It still restricts results that would expose that opened type through an invariant generic container. SE-0352 also records broader opening behavior enabled in Swift 6 language mode. See Implicitly Opened Existentials.

For example, the following diagnostic fragment adds a generic wrapper to the program above. The commented call illustrates the problematic boundary; it is not an additional application entry point:

@MainActor
struct ProviderRow<P: ViewProvider>: View {
    let provider: P

    var body: some View {
        provider.makeView()
    }
}

@MainActor
func inspectBoundary(_ provider: any ViewProvider) {
    // Problematic: the constructed result would retain the opened P.
    // let row = ProviderRow(provider: provider)

    // Supported boundary: the result is the concrete type AnyView.
    let row = erasedView(for: provider)
    _ = row
}

This explains why adding a generic layer alone may leave the error unresolved. Inspect both its parameter and its result. A helper that returns a concrete erased view finishes the conversion within the call; a result such as ProviderRow<P> retains the provider type in its structure.

Likewise, some View is not a spelling for a return value whose underlying type may vary arbitrarily at runtime. The implementation chooses a consistent underlying type, potentially dependent on generic arguments. A builder can encode branches into a composed result, but an opaque result declaration by itself does not unify unrelated return types. See Opaque and Boxed Protocol Types.

When should you erase the provider itself?

Choose a concrete wrapper when downstream interfaces should receive one provider type, or when you want callers to normalize providers before storing them. The following additive declaration works with the sample’s existing protocol and providers. It captures the provider and creates its view when makeView() is called.

@MainActor
struct AnyViewProvider: ViewProvider {
    let id: String
    private let build: @MainActor () -> AnyView

    init<P: ViewProvider>(_ provider: P) {
        id = provider.id
        build = { AnyView(provider.makeView()) }
    }

    func makeView() -> AnyView {
        build()
    }
}

To use this alternative, change both stored providers properties in the complete program to [AnyViewProvider]. Replace the app’s initial array with the following expression:

[
    AnyViewProvider(
        TitleProvider(id: "title", title: "Component gallery")
    ),
    AnyViewProvider(SwatchProvider(id: "swatch"))
]

Then replace erasedView(for: provider) in the gallery’s ForEach closure with provider.makeView(). The wrapper already returns AnyView, so that call needs no additional conversion. This implementation copies the identifier and captures the supplied provider value; it is not a mechanism for replacing that captured value later.

If every consumer needs erased output, a simpler protocol can require makeView() -> AnyView directly. That reduces adapter code but makes every conformer responsible for erasure. Prefer it when erased output is an intentional interface promise. Prefer the associated-type contract when generic consumers should still be able to use each provider’s output relationship.

Preserve compiler limits and view identity

Keep language features separate from builder behavior. The sample relies on Swift 5.7 existential features and an explicit AnyView result. Apple’s current documentation distinguishes Xcode 26 and earlier from Xcode 27 and later: the latter constructs type-agnostic builder content and recommends ContentBuilder for that behavior. Its documentation still describes type safety through conditional conformances. Do not interpret relaxed builder input checks as a general promise that an existential itself satisfies every generic View constraint. See ViewBuilder and ContentBuilder.

For a fixed set of components, consider an enum whose cases carry component data and whose rendering property switches over those cases. This makes the supported alternatives explicit. It is a useful design when adding a new component should require updating one central renderer; the provider approach fits interfaces where conforming implementations should supply their own rendering.

Type erasure also has a documented identity consequence. Apple states that changing the type wrapped by an AnyView destroys the previous hierarchy and creates a new one. A stable collection identifier does not override that behavior. If a component can switch between different wrapped view types, check its state and transition behavior in the actual app. See AnyView.

For this gallery, keep the associated-type provider contract, store the mixed values as existentials, and return AnyView from one rendering helper. Move erasure into a concrete provider wrapper when downstream interfaces need that uniform type. This leaves a clear boundary to inspect whenever the collection, rendering contract, or compiler configuration changes.

Sources