Declare caller-supplied content as (Input) -> Content, with Content constrained to View. This lets a macOS component supply a typed value while callers choose its presentation. The contract requires accepting that input; it cannot require using it.

This tutorial builds a small desktop swatch preview. The reusable component owns a size control and supplies a synthetic configuration to the caller’s rendering closure. The caller draws the swatch without managing the component’s internal state.

What interface expresses the requirement?

Use a named generic parameter for the returned view and a concrete type for the component’s input. In the example below, the essential signature is @ViewBuilder content: @escaping (SwatchInput) -> Content, inside a declaration constrained by Content: View.

The input and output have separate jobs. SwatchInput defines the values the component supplies. Content represents the concrete view type produced by each caller’s closure. Named generic parameters let a declaration express and reuse these type relationships, as described in Swift’s Generics chapter.

Apple uses this interface pattern in GeometryReader: its initializer accepts a closure of type (GeometryProxy) -> Content. The container supplies the proxy, and the caller builds content from it. That is a useful precedent for a custom component that supplies its own configuration. See Apple’s GeometryReader and init(content:) documentation.

Choose the contract according to the actual task:

Requirement Suitable interface
Caller supplies content without component input () -> Content
Component supplies a specific configuration (SwatchInput) -> Content
Container receives a value whose type varies between uses A named Input generic parameter, with a matching (Input) -> Content closure
Compiler must prove the caller uses the configuration A closure signature does not express that requirement

These choices follow from Swift’s function types: their parameter types and return type describe what can be passed and returned. They do not describe how the implementation must use its arguments. See the Swift language reference’s Function Type section.

Build a complete macOS swatch preview

Use a macOS app target with SwiftUI. The example uses the established @ViewBuilder spelling and is intended for Swift 5.7 or later, with a macOS 12 or later deployment target. It avoids preview macros and newer content-builder names. Swift and the Apple SDK are unavailable in the research environment, so this program was inspected but was not compiled or run here.

In an Xcode SwiftUI app project, replace the generated app declaration with the following complete program. Remove the generated ContentView declaration if it would duplicate this one. Keep exactly one app entry point marked @main. Apple’s Xcode help documents starting with a SwiftUI project or adding a SwiftUI file to an existing project.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

The expected initial display contains the Large sample toggle, the title Sample A, and a blue rounded square with a side length of 64 points. Turning on the toggle changes the supplied side length to 120 points and adds Large preview. Those sizes and labels are illustrative local values, not measured results.

The component owns isLarge. Its computed input packages the current selection, and content(input) passes that package to the caller. SwiftUI manages state storage and updates the parts of the view hierarchy that depend on a changed value, as documented under State.

The caller owns the presentation: a title, a shape, and an optional caption. It can substitute another arrangement while keeping the same typed input. Both the panel and its caller implement body to compose their interfaces, following Apple’s documented View model.

Why are the builder and escaping annotations needed?

@ViewBuilder lets the caller write several view expressions and supported conditional branches in one closure. In this example, the caller supplies two unconditional views and one conditional caption. The builder combines that content into a result that satisfies the component’s return constraint. Apple documents multiple child views and conditional construction under ViewBuilder.

The builder does not supply SwatchInput. The ordinary closure parameter declares the input, and content(input) supplies the actual argument. If the initializer accepted () -> Content, adding a builder annotation would not turn it into a closure that accepts configuration.

@escaping is needed because the initializer stores the closure in a property for later invocation from body. Swift’s Closures chapter explains that a closure escapes when it can be called after the receiving function returns.

For a component whose configuration is already final at initialization, another design is to invoke a nonescaping builder immediately and store the resulting Content value. That changes when configuration is applied. Here, retaining the closure makes the intended flow explicit: derive the current input from component state, then build content using that input.

Preserve the Xcode builder boundary

Apple’s current documentation distinguishes Xcode 26 and earlier from Xcode 27 and later. For the earlier tools, it describes ViewBuilder as building views. With Xcode 27 and later, it describes type-agnostic construction and recommends the ContentBuilder alias to indicate that broader behavior. See ViewBuilder.

When maintaining an Xcode 27-or-later codebase, the initializer can use @ContentBuilder instead. Apple documents it as a type alias for ViewBuilder, with conformance enforced through the content types it produces. Keep Content: View on this component: its output is still intended for a view hierarchy. Do not assume the newer spelling exists in older SDKs simply because the current documentation lists older deployment targets. See ContentBuilder.

Why doesn’t some View solve every part of this contract?

some View is appropriate for the example’s body properties. It hides the concrete result type while preserving the promise that the result is a view. The stored closure instead uses the named Content parameter so the component can refer to the same result type in its property and initializer.

Swift 5.7 introduced opaque parameter declarations through SE-0341. A direct parameter such as _ value: some View introduces an unnamed generic parameter whose concrete type is determined by the caller. It does not mean that the function implementation can choose an arbitrary view and pass it wherever that unnamed type is expected. See Opaque Parameter Declarations.

In particular, a callback parameter written as (some View) -> Content is outside the supported opaque-parameter positions documented by SE-0341. The proposal explicitly prohibits opaque types in the input position of a callback’s function type. Its discussion of potentially enabling that syntax appears under future directions, not implemented behavior. The current Types reference retains that restriction.

For this desktop task, the supported path is straightforward: make the component-supplied value a concrete configuration type. If a different reusable container must preserve a caller-provided input type, name that type with a generic parameter and carry it consistently through the stored value and callback. Neither design requires an opaque callback input.

What does the compiler actually guarantee?

The signature guarantees a callable shape: one SwatchInput argument and a result satisfying Content: View. It also gives the closure parameter its inferred type, so the caller can access input.title, input.side, and input.color without repeating a type annotation. Swift documents this inference from closure context in Closures.

It cannot guarantee that those properties influence the display. For example, this is a valid replacement expression inside ContentView.body; it deliberately accepts and ignores the configuration:

SwatchPanel { _ in
    Text("Fixed preview")
}

That boundary matters when writing the component’s requirements. “Caller content accepts SwatchInput” is a type contract. “Caller content displays the supplied size” is a behavioral requirement. If displaying that size is essential to the component, put that rendering inside the component and expose a smaller customization area.

Use the following checks to diagnose integration problems. They are inspection steps derived from the interface, not reported compiler-test results:

Symptom Check
Missing argument when invoking content Pass the configuration: content(input).
Escaping-closure diagnostic in the initializer Keep @escaping when storing the callback.
Several sibling views fail in the caller Keep the appropriate builder annotation on the initializer parameter.
An explicitly typed callback is rejected Check that its input is SwatchInput, rather than an unrelated type such as String.
The preview ignores the size toggle Check whether the rendering reads input.side.
The component keeps showing initial configuration Check whether content was constructed once and stored instead of invoking the callback with current input.

Keep the contract as small as the task allows. A concrete configuration and a generic view result give this swatch panel a clear boundary: the component supplies current values, the caller receives them with full type information, and the chosen presentation remains easy to replace.

Sources