Declara el contenido proporcionado por quien llama como (Input) -> Content, con Content restringido a View. Esto permite que un componente de macOS proporcione un valor tipado mientras quienes llaman eligen cómo presentarlo. El contrato exige aceptar esa entrada; no puede exigir que se use.
Este tutorial crea una pequeña vista previa de muestras para escritorio. El componente reutilizable tiene un control de tamaño y proporciona una configuración sintética al cierre de renderizado de quien llama. Quien llama dibuja la muestra sin administrar el estado interno del componente.
¿Qué interfaz expresa el requisito?
Usa un parámetro genérico con nombre para la vista devuelta y un tipo concreto para la entrada del componente. En el ejemplo siguiente, la firma esencial es @ViewBuilder content: @escaping (SwatchInput) -> Content, dentro de una declaración restringida por Content: View.
La entrada y la salida tienen funciones distintas. SwatchInput define los valores que proporciona el componente. Content representa el tipo concreto de vista que produce el cierre de cada persona que llama. Los parámetros genéricos con nombre permiten que una declaración exprese y reutilice estas relaciones entre tipos, como se describe en el capítulo Generics de Swift.
Apple utiliza este patrón de interfaz en GeometryReader: su inicializador acepta un cierre de tipo (GeometryProxy) -> Content. El contenedor proporciona el proxy y quien llama crea contenido a partir de él. Es un precedente útil para un componente personalizado que proporciona su propia configuración. Consulta la documentación de Apple sobre GeometryReader e init(content:).
Elige el contrato según la tarea concreta:
| Requisito | Interfaz adecuada |
|---|---|
| Quien llama proporciona contenido sin entrada del componente | () -> Content |
| El componente proporciona una configuración específica | (SwatchInput) -> Content |
| El contenedor recibe un valor cuyo tipo varía entre usos | Un parámetro genérico Input con nombre y un cierre (Input) -> Content correspondiente |
| El compilador debe demostrar que quien llama usa la configuración | La firma de un cierre no expresa ese requisito |
Estas opciones se derivan de los tipos de función de Swift: sus tipos de parámetros y el tipo de retorno describen qué se puede pasar y devolver. No describen cómo debe usar la implementación sus argumentos. Consulta la sección Function Type de la referencia del lenguaje Swift.
Crea una vista previa completa de muestras para macOS
Usa un destino de app para macOS con SwiftUI. El ejemplo emplea la sintaxis establecida @ViewBuilder y está pensado para Swift 5.7 o posterior, con un destino de implementación macOS 12 o posterior. Evita las macros de vista previa y los nombres más recientes para constructores de contenido. Swift y el SDK de Apple no están disponibles en el entorno de investigación, así que se inspeccionó este programa, pero no se compiló ni ejecutó aquí.
En un proyecto de app SwiftUI de Xcode, sustituye la declaración de app generada por el siguiente programa completo. Elimina la declaración ContentView generada si esta duplicaría la que aparece aquí. Mantén exactamente un punto de entrada de la app marcado con @main. La ayuda de Xcode de Apple explica cómo empezar con un proyecto SwiftUI o añadir un archivo SwiftUI a un proyecto existente.
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()
}
}
}
La pantalla inicial esperada contiene el conmutador Large sample, el título Sample A y un cuadrado azul redondeado con lados de 64 puntos. Al activar el conmutador, el tamaño proporcionado cambia a 120 puntos y aparece Large preview. Esos tamaños y etiquetas son valores locales ilustrativos, no resultados de mediciones.
El componente es propietario de isLarge. Su propiedad calculada input reúne la selección actual, y content(input) pasa ese conjunto a quien llama. SwiftUI administra el almacenamiento del estado y actualiza las partes de la jerarquía de vistas que dependen de un valor modificado, como se documenta en State.
Quien llama es responsable de la presentación: un título, una forma y un subtítulo opcional. Puede sustituirlos por otra disposición mientras mantiene la misma entrada tipada. Tanto el panel como quien lo llama implementan body para componer sus interfaces, según el modelo View documentado por Apple.
¿Por qué se necesitan las anotaciones del constructor y de escape?
@ViewBuilder permite que quien llama escriba varias expresiones de vista y ramas condicionales admitidas en un solo cierre. En este ejemplo, quien llama proporciona dos vistas incondicionales y un subtítulo condicional. El constructor combina ese contenido en un resultado que satisface la restricción de retorno del componente. Apple documenta varias vistas secundarias y la construcción condicional en ViewBuilder.
El constructor no proporciona SwatchInput. El parámetro ordinario del cierre declara la entrada, y content(input) proporciona el argumento real. Si el inicializador aceptara () -> Content, añadir una anotación de constructor no lo convertiría en un cierre que acepta configuración.
Se necesita @escaping porque el inicializador guarda el cierre en una propiedad para invocarlo más adelante desde body. El capítulo Closures de Swift explica que un cierre escapa cuando puede invocarse después de que retorne la función receptora.
Para un componente cuya configuración ya es definitiva durante la inicialización, otra opción es invocar de inmediato un constructor sin escape y guardar el valor Content resultante. Eso cambia el momento en que se aplica la configuración. Aquí, conservar el cierre hace explícito el flujo previsto: derivar la entrada actual del estado del componente y, luego, crear contenido con esa entrada.
Mantén el límite entre versiones de Xcode para el constructor
La documentación actual de Apple distingue entre Xcode 26 y versiones anteriores, y Xcode 27 y posteriores. Para las herramientas anteriores, describe ViewBuilder como un constructor de vistas. Con Xcode 27 y versiones posteriores, describe una construcción agnóstica al tipo y recomienda el alias ContentBuilder para indicar ese comportamiento más amplio. Consulta ViewBuilder.
Al mantener una base de código para Xcode 27 o posterior, el inicializador puede usar @ContentBuilder. Apple lo documenta como un alias de tipo para ViewBuilder, cuya conformidad se aplica mediante los tipos de contenido que produce. Mantén Content: View en este componente: su salida sigue destinada a una jerarquía de vistas. No des por hecho que la sintaxis más reciente existe en SDK antiguos solo porque la documentación actual enumere destinos de implementación anteriores. Consulta ContentBuilder.
¿Por qué some View no resuelve todas las partes de este contrato?
some View es adecuado para las propiedades body del ejemplo. Oculta el tipo concreto del resultado y, al mismo tiempo, conserva la garantía de que el resultado es una vista. En cambio, el cierre almacenado usa el parámetro con nombre Content, para que el componente pueda referirse al mismo tipo de resultado en su propiedad y en su inicializador.
Swift 5.7 introdujo las declaraciones de parámetros opacos mediante SE-0341. Un parámetro directo como _ value: some View introduce un parámetro genérico sin nombre cuyo tipo concreto determina quien llama. No significa que la implementación de la función pueda elegir una vista arbitraria y pasarla allí donde se espera ese tipo sin nombre. Consulta Opaque Parameter Declarations.
En particular, un parámetro de devolución de llamada escrito como (some View) -> Content no está entre las posiciones admitidas para parámetros opacos que documenta SE-0341. La propuesta prohíbe expresamente los tipos opacos en la posición de entrada del tipo de función de una devolución de llamada. Su análisis de la posibilidad de habilitar esa sintaxis aparece entre las direcciones futuras, no como comportamiento implementado. La referencia actual de Types mantiene esa restricción.
Para esta tarea de escritorio, la solución admitida es sencilla: haz que el valor proporcionado por el componente sea un tipo de configuración concreto. Si otro contenedor reutilizable debe conservar el tipo de entrada proporcionado por quien llama, nómbralo con un parámetro genérico y mantenlo de forma coherente en el valor almacenado y en la devolución de llamada. Ninguna de las dos opciones requiere una entrada opaca en la devolución de llamada.
¿Qué garantiza realmente el compilador?
La firma garantiza una forma invocable: un argumento SwatchInput y un resultado que satisface Content: View. También asigna al parámetro del cierre su tipo inferido, de modo que quien llama puede acceder a input.title, input.side y input.color sin repetir una anotación de tipo. Swift documenta esta inferencia a partir del contexto del cierre en Closures.
No puede garantizar que esas propiedades influyan en lo que se muestra. Por ejemplo, esta es una expresión de reemplazo válida dentro de ContentView.body; acepta e ignora deliberadamente la configuración:
SwatchPanel { _ in
Text("Fixed preview")
}
Ese límite importa al definir los requisitos del componente. «El contenido de quien llama acepta SwatchInput» es un contrato de tipos. «El contenido de quien llama muestra el tamaño proporcionado» es un requisito de comportamiento. Si mostrar ese tamaño es esencial para el componente, coloca ese renderizado dentro del componente y ofrece un área de personalización más pequeña.
Usa las siguientes comprobaciones para diagnosticar problemas de integración. Son pasos de inspección derivados de la interfaz, no resultados de pruebas del compilador:
| Síntoma | Comprobación |
|---|---|
Falta un argumento al invocar content |
Pasa la configuración: content(input). |
| Error de cierre con escape en el inicializador | Mantén @escaping cuando guardes la devolución de llamada. |
| Varias vistas hermanas producen un error en el código de quien llama | Mantén la anotación de constructor adecuada en el parámetro del inicializador. |
| Se rechaza una devolución de llamada con tipo explícito | Comprueba que su entrada sea SwatchInput y no un tipo distinto, como String. |
| La vista previa ignora el conmutador de tamaño | Comprueba si el renderizado lee input.side. |
| El componente sigue mostrando la configuración inicial | Comprueba si el contenido se creó una sola vez y se guardó, en lugar de invocar la devolución de llamada con la entrada actual. |
Mantén el contrato tan sencillo como permita la tarea. Una configuración concreta y un resultado de vista genérico dan a este panel de muestras un límite claro: el componente proporciona los valores actuales, quien llama los recibe con toda la información de tipos y la presentación elegida sigue siendo fácil de cambiar.
Fuentes
- Generics — Swift
- Types — Swift
- Closures — Swift
- Opaque Parameter Declarations — Swift Evolution
- GeometryReader — Apple
- init(content:) — Apple
- ViewBuilder — Apple
- ContentBuilder — Apple
- View — Apple
- State — Apple
- Show a preview of your user interface — Apple