Haz que tu componente de SwiftUI sea genérico respecto al tipo que conforma al protocolo y declara después su vista almacenada como Provider.Content. Así se conserva en todo el componente la relación entre el proveedor y su tipo de vista asociado.

En este tutorial se crea una pequeña ventana de macOS con dos mosaicos sintéticos. El objetivo es almacenar un proveedor y la vista que produce sin reemplazar el tipo asociado por any View o AnyView. El mismo patrón sirve cuando un componente de escritorio reutilizable acepta una etiqueta, un accesorio o un proveedor de contenido personalizado.

¿Qué relación deben conservar las propiedades almacenadas?

Un tipo asociado nombra un tipo elegido por cada conformidad de protocolo. Si un protocolo declara associatedtype Content: View, quien lo conforme debe proporcionar un tipo de contenido concreto. El código genérico puede referirse a ese tipo como Provider.Content, incluso cuando la implementación está oculta tras un resultado opaco. Swift documenta los tipos asociados y las restricciones genéricas en el capítulo sobre genéricos.

Por tanto, el componente necesita dos declaraciones vinculadas: un parámetro genérico restringido al protocolo del proveedor y una propiedad de contenido almacenada cuyo tipo sea el tipo asociado de ese parámetro. Declarar dos propiedades independientes que solo conformen a protocolos no expresa esta conexión.

SwiftUI también usa un contrato relacionado: View declara associatedtype Body: View, y Swift infiere ese tipo asociado a partir de la implementación de body de una vista personalizada. Content del proveedor y Body del componente son tipos asociados distintos; no tienen que ser iguales. Consulta la documentación de Body de Apple.

Crea un ejemplo completo para macOS

Para este ejemplo, usa un proyecto de app de macOS en Xcode con SwiftUI y un destino de implementación de macOS 13 o posterior. Reemplaza las declaraciones de app y vista de contenido generadas con el siguiente archivo único, manteniendo exactamente una declaración @main en el destino. Apple documenta esta estructura de entrada de la app y el uso de WindowGroup en App.

El ejemplo usa deliberadamente la sintaxis habitual de tipos asociados y genéricos con nombre. Los resultados opacos se implementaron en Swift 5.1; la sintaxis opcional de tipos asociados primarios que se explica más adelante requiere Swift 5.7. El archivo completo está pensado para una cadena de herramientas actual de Swift 6. Se contrastó con la documentación consultada, pero no se compiló aquí porque este entorno no tiene un compilador de Swift. El destino de macOS elegido es un requisito del ejemplo, no una afirmación sobre la versión más antigua del sistema operativo compatible con todas las funciones subyacentes. Consulta SE-0244 y SE-0346.

import SwiftUI

@MainActor
protocol TileProvider {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

@MainActor
struct TextTile: TileProvider {
    let title: String
    let message: String

    func makeContent() -> Text {
        Text(message)
    }
}

@MainActor
struct CircleTile: TileProvider {
    let title: String
    let caption: String

    func makeContent() -> some View {
        HStack(spacing: 8) {
            Circle()
                .fill(Color.blue)
                .frame(width: 16, height: 16)
            Text(caption)
        }
    }
}

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider
    private let content: Provider.Content

    init(provider: Provider) {
        self.provider = provider
        self.content = provider.makeContent()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            content
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        VStack(spacing: 12) {
            TileCard(provider: TextTile(
                title: "Text tile",
                message: "Blue square"
            ))
            TileCard(provider: CircleTile(
                title: "Shape tile",
                caption: "Blue circle"
            ))
        }
        .padding()
        .frame(width: 360)
    }
}

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

Compila y ejecuta el destino de macOS. El resultado previsto es una ventana que contiene un mosaico de texto y otro con un círculo azul junto a su leyenda. Ambos usan el mismo componente genérico; el tipo del proveedor se infiere de forma independiente en cada punto de llamada.

Las anotaciones explícitas @MainActor mantienen estas declaraciones que generan la interfaz de usuario en el mismo contexto de actor. La documentación actual de View de Apple declara que el protocolo tiene aislamiento del actor principal. Las anotaciones también hacen explícito el contexto previsto del protocolo de proveedor personalizado.

¿Por qué el componente genérico conserva el tipo?

En la primera tarjeta, Provider es TextTile, por lo que Provider.Content es Text. En la segunda tarjeta, Provider es CircleTile, y su tipo de contenido asociado es el resultado opaco de CircleTile.makeContent(). Puedes nombrar ese tipo mediante CircleTile.Content sin escribir el tipo anidado de diseño de SwiftUI.

La asignación esencial es self.content = provider.makeContent(). El protocolo garantiza que este método devuelva exactamente Provider.Content, que coincide exactamente con el tipo declarado de la propiedad almacenada. Se trata de una relación en tiempo de compilación derivada de la restricción genérica, no de una comparación de tipos en tiempo de ejecución. El capítulo sobre genéricos de Swift explica cómo acceder a los tipos asociados mediante parámetros genéricos restringidos.

Usar some View en la implementación que conforma al protocolo no elimina esta relación. Un resultado opaco oculta la implementación concreta, pero conserva la identidad de su tipo. Por eso sirve como testigo de un tipo asociado. Swift explica esta diferencia en Tipos de protocolo opacos y en caja.

Estas tarjetas tienen especializaciones distintas: TileCard<TextTile> y TileCard<CircleTile>. Reutilizar la declaración de origen no exige que todas las tarjetas tengan un único tipo de contenido almacenado universal.

¿Por qué no basta con usar propiedades opacas o existenciales independientes?

Los siguientes fragmentos son ejemplos para diagnosticar problemas, no sustitutos del programa completo:

// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View

// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider

// Diagnostic fragment: stores an existential view value.
let content: any View

Una propiedad almacenada opaca necesita un inicializador en su declaración que establezca su tipo subyacente. Escribir some View no crea un parámetro genérico elegido por quien llama que un inicializador arbitrario del componente pueda rellenar más tarde. Por ejemplo, let content: some View = Text("Sample") establece un tipo para esa declaración; no vincula la propiedad al tipo asociado de otra propiedad. Esta regla para las propiedades almacenadas está documentada en SE-0244: Tipos de resultado opaco.

Un existencial como any TileProvider permite almacenar distintos tipos que conforman al protocolo. Oculta el tipo concreto que lo conforma, de modo que la declaración ya no expone la relación como Provider.Content. Del mismo modo, any View es un contenedor existencial; no ofrece el mismo contrato que un tipo genérico con nombre que conforma a View. Consulta la explicación de Swift sobre los tipos de protocolo en caja.

Swift puede abrir implícitamente argumentos existenciales en algunas llamadas genéricas. Eso no significa que todos los existenciales puedan usarse en cualquier inicializador genérico. En concreto, SE-0352 restringe la apertura cuando el tipo subyacente desconocido escaparía dentro de un resultado genérico como X<T>. Una especialización del componente genérico puede encontrar precisamente ese límite. Mantén el proveedor como genérico antes de crear la tarjeta, en lugar de asumir que un existencial siempre puede convertirse de nuevo en una especialización persistente. Consulta SE-0352: Apertura implícita de existenciales.

Elige el almacenamiento según cuándo deba producirse el contenido

El ejemplo llama a makeContent() durante cada inicialización de TileCard y almacena el valor de vista devuelto. No crea una caché persistente entre inicializaciones posteriores del componente. Mantener ambas propiedades demuestra la relación de tipos solicitada, pero un componente que solo necesite la vista resultante podría omitir el proveedor almacenado.

Si el componente debe solicitar contenido cada vez que se evalúa su body, conserva solo el proveedor. La siguiente declaración de componente sustituye a la anterior y se usa con el protocolo y la app de arriba:

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider

    init(provider: Provider) {
        self.provider = provider
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            provider.makeContent()
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

Ambos diseños conservan el tipo asociado. La diferencia depende directamente de dónde aparece la llamada al método: durante la inicialización en el primer diseño y durante la evaluación del cuerpo en el segundo. Elige ese momento de manera deliberada y evita que la producción de contenido dependa de que se invoque exactamente una vez.

Usa esta guía de decisión para adaptar el ejemplo:

Requisito del componente Estrategia de declaración Relación de tipos
Almacenar el proveedor y la vista que produce Proveedor genérico más Provider.Content Conserva tanto la identidad del proveedor como su tipo de contenido
Producir contenido durante la evaluación del cuerpo Almacenar el proveedor genérico Conserva el tipo asociado sin almacenar un valor de contenido por separado
Almacenar proveedores con un tipo de contenido común conocido Considerar un existencial restringido Conserva la igualdad de contenido especificada y oculta la identidad del proveedor
Borrar deliberadamente implementaciones de vista distintas Usar AnyView en un límite elegido Almacena el tipo envoltorio en vez de exponer el tipo de vista original

¿Los tipos asociados primarios eliminan la necesidad de genéricos?

Los tipos asociados primarios ofrecen una forma más breve de expresar ciertas restricciones de igualdad de tipos. Si eres dueño del protocolo, esta declaración alternativa hace que Content sea primario; los demás requisitos del protocolo se mantienen:

// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

Con esa declaración, any TileProvider<Text> describe un existencial cuyo tipo de contenido asociado se sabe que es Text. Distintas implementaciones de proveedor pueden compartir ese tipo de almacenamiento solo si sus conformidades exponen exactamente esa igualdad de contenido. La sintaxis restringe un tipo asociado; no convierte el protocolo en una estructura genérica. Consulta SE-0346 y SE-0353.

Esto no combina tipos de vista desconocidos y arbitrarios en un único tipo concreto conservado. En particular, CircleTile expone un resultado de contenido opaco, por lo que quienes llaman no pueden tratarlo simplemente como Text. Usa el componente genérico original cuando el contenido asociado del proveedor deba seguir sin restricciones.

Los existenciales restringidos se implementaron en Swift 5.7, y SE-0353 menciona explícitamente limitaciones de tiempo de ejecución y de implementación en versiones anteriores. Por tanto, el soporte del compilador por sí solo no basta para establecer la compatibilidad con el destino de implementación de ese diseño opcional. Comprueba los diagnósticos de disponibilidad de la cadena de herramientas de destino antes de adoptarlo. El ejemplo principal no depende del almacenamiento de existenciales restringidos. Consulta la discusión sobre ABI de SE-0353.

¿Qué debes comprobar si falla la compilación?

Sigue el tipo desde el requisito del protocolo hasta la propiedad almacenada antes de cambiar el diseño:

  1. Confirma que el protocolo declara un tipo asociado restringido a View.
  2. Confirma que el componente tiene un parámetro de proveedor genérico con nombre, restringido a ese protocolo.
  3. Declara el contenido almacenado como Provider.Content, de acuerdo con el tipo de retorno del método.
  4. Comprueba si un elemento contenedor ya convirtió el proveedor en any TileProvider.
  5. Si añades un modificador antes del almacenamiento, comprueba su tipo de resultado: es posible que la vista modificada ya no sea Provider.Content.
  6. Comprueba por separado los diagnósticos de aislamiento de actor y los del tipo asociado.

Apple documenta que los modificadores de vista envuelven una vista en otra vista. Por consiguiente, asignar provider.makeContent().padding() a una propiedad declarada como Provider.Content cambia el contrato que debe satisfacer la asignación. Almacena el contenido original y aplica los modificadores de presentación en body, o rediseña el tipo almacenado para que describa el resultado modificado. Consulta View.

Los constructores también tienen límites según la versión. La documentación actual de Apple describe el comportamiento de ViewBuilder para Xcode 26 y versiones anteriores, y luego un comportamiento distinto para la creación de contenido a partir de Xcode 27. El ejemplo evita un requisito de constructor personalizado: cada proveedor devuelve una expresión explícita. Si lo amplías con API de proveedor basadas en constructores, sigue la documentación de la versión de Xcode con la que compiles. Consulta ViewBuilder.

Si eliges AnyView de forma deliberada, ten en cuenta su comportamiento documentado: al cambiar el tipo de vista envuelta, se destruye la jerarquía anterior y se crea una nueva. Esa es una decisión de diseño independiente de conservar un tipo asociado en el almacenamiento. Apple lo documenta en AnyView.

Para este componente, conserva la relación en su límite de almacenamiento: un Provider genérico, un resultado de tipo Provider.Content y una propiedad almacenada de ese mismo tipo. Así podrás cambiar el diseño interno de un proveedor y el compilador seguirá propagando su tipo de vista asociado por todo el componente.

Fuentes