Para una ventana de macOS que muestra componentes heterogéneos de SwiftUI, mantén fuertemente tipizada la salida de cada proveedor y bórrale el tipo cuando la interfaz de renderizado compartida necesite un único tipo de vista concreto. Recurre antes al borrado de tipos del proveedor cuando la propia colección necesite un envoltorio uniforme.
Este artículo crea una pequeña galería de componentes que contiene texto sintético y una muestra azul. La tarea consiste en almacenar juntas distintas implementaciones de proveedores y renderizarlas mediante una única interfaz de SwiftUI. La decisión de diseño es dónde dejar de exponer la relación entre un proveedor y su salida.
¿Qué debe preservar el contrato del proveedor?
Un proveedor con un tipo de salida asociado indica que cada tipo que adopta el protocolo elige su propio tipo de vista. Una implementación concreta puede ocultar esa elección detrás de some View, mientras que los consumidores genéricos conservan la relación entre el proveedor y su salida. Los tipos opacos preservan la identidad del tipo en Swift; los tipos de protocolo en caja permiten distintos tipos subyacentes detrás de una interfaz compartida. Consulta Tipos de protocolo opacos y en caja.
Lo siguiente es un fragmento de contrato, no una aplicación completa:
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)
}
}
Aquí, TitleProvider.Content se infiere a partir del tipo de retorno opaco de makeView(). El protocolo exige un tipo asociado y la implementación lo proporciona mediante inferencia. SE-0244 documenta explícitamente este patrón admitido y la restricción que impide declarar un tipo de retorno opaco directamente como requisito de un protocolo. Por tanto, sustituir el requisito por func makeView() -> some View no es el contrato admitido que describe esa propuesta. Consulta Tipos de resultado opacos.
Un arreglo de any ViewProvider resuelve el almacenamiento heterogéneo. SE-0309 de Swift 5.7 permite el uso existencial de protocolos con tipos asociados, aunque mantiene restricciones sobre las operaciones de miembros individuales. Al leer una salida a través de ese existencial, su tipo asociado puede borrarse hasta el límite superior de su protocolo, que en este caso es any View. Consulta Desbloquear los existenciales para todos los protocolos.
Esto plantea dos decisiones distintas: cómo almacenar proveedores variados y qué tipo concreto devuelve el renderizador. Apple documenta AnyView como una vista concreta con el tipo borrado que adopta View, por lo que es una opción explícita para la segunda decisión. Consulta AnyView.
Elige el límite según los requisitos del consumidor
Usa esta tabla de decisiones antes de cambiar el protocolo. Estas recomendaciones aplican los contratos de tipos documentados a la tarea de la galería; son pautas de diseño, no resultados de rendimiento medidos.
| Requisito del consumidor | Diseño adecuado | Dónde se oculta la información de tipos |
|---|---|---|
| Renderizar un proveedor concreto mediante código reutilizable | Un consumidor genérico restringido a ViewProvider |
Mantener disponible la relación entre el proveedor y la salida |
| Almacenar distintos proveedores y conservar sus metadatos compartidos | [any ViewProvider], con una función auxiliar de renderizado que devuelve AnyView |
El tipo del proveedor al almacenar; el tipo de salida al renderizar |
| Exponer un envoltorio concreto del proveedor al código posterior | [AnyViewProvider] |
Al construir el envoltorio |
| Exigir que cada implementación devuelva una vista cuyo tipo ya esté borrado | Un requisito del protocolo que devuelve AnyView |
Dentro de cada implementación |
| Mostrar un conjunto fijo y pequeño de alternativas de componentes | Un enum y un switch basado en un constructor |
Las alternativas siguen siendo explícitas en el enum |
Para esta galería, elige almacenamiento existencial y borrado del tipo de salida. Los proveedores conservan sus propios tipos de salida, la colección expone un identificador de cadena estable y una función auxiliar se encarga de convertir el tipo a AnyView. Así, cambiar la estructura interna de un componente no exige añadir código de borrado de tipos en ese componente.
Un parámetro genérico para la colección serviría para otra tarea. Un arreglo cuyo tipo de elemento sea un único tipo genérico de proveedor no puede representar automáticamente varios tipos de proveedor sin relación entre sí. La apertura implícita de existenciales de Swift funciona con un argumento existencial individual; no proporciona a un arreglo heterogéneo un único tipo subyacente que adopte el protocolo. Consulta Existenciales abiertos implícitamente.
Crea una galería completa de componentes para macOS
El ejemplo requiere un compilador Swift 5.7 o posterior y un destino de aplicación SwiftUI para macOS. Configura el destino de implementación de la muestra en macOS 12 o posterior. En un proyecto de aplicación para macOS con SwiftUI creado en Xcode, sustituye la declaración de la aplicación generada por el programa completo siguiente y conserva exactamente una declaración @main.
Las anotaciones explícitas @MainActor mantienen la creación y el renderizado de proveedores en el mismo actor. La declaración actual de View de Apple está aislada al actor principal. Su documentación también explica que las vistas personalizadas proporcionan su contenido mediante body. Consulta 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)
}
}
}
El resultado esperado es una ventana que muestra Component gallery sobre un rectángulo redondeado azul. Todas las entradas son valores sintéticos locales. El ejemplo se revisó, pero no se compiló aquí porque el entorno disponible no tiene un compilador Swift ni el SDK de Apple.
El resultado de la función auxiliar siempre es AnyView, independientemente del tipo de salida asociado del proveedor. Dentro de la función, el proveedor es genérico y se conserva su relación con Content. Apple documenta que el inicializador usado aquí acepta un valor genérico restringido a View y borra su tipo. Consulta AnyView.init(_:).
La colección usa id: \.id porque el protocolo fija id como String; no exige recuperar otro tipo asociado desconocido. La documentación de ForEach de Apple admite tanto elementos identificables como una ruta de clave de identificador explícita. Mantén los identificadores únicos dentro de esta colección y estables para los componentes lógicos entre actualizaciones. Consulta ForEach.
¿Por qué funciona una función auxiliar genérica cuando falla un envoltorio de vista genérico?
La propiedad importante de erasedView es su tipo de retorno concreto. Swift 5.7 introdujo la apertura implícita de argumentos existenciales, que permite al compilador vincular un parámetro genérico al tipo subyacente del valor existencial en una llamada. Aun así, restringe los resultados que expondrían ese tipo abierto mediante un contenedor genérico invariante. SE-0352 también recoge un comportamiento de apertura más amplio, habilitado en el modo de lenguaje Swift 6. Consulta Existenciales abiertos implícitamente.
Por ejemplo, el siguiente fragmento de diagnóstico añade un envoltorio genérico al programa anterior. La llamada comentada ilustra el límite problemático; no es un punto de entrada adicional de la aplicación:
@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
}
Esto explica por qué añadir una capa genérica por sí sola puede no resolver el error. Examina tanto su parámetro como su resultado. Una función auxiliar que devuelve una vista concreta con el tipo borrado completa la conversión durante la llamada; un resultado como ProviderRow<P> conserva el tipo del proveedor en su estructura.
Del mismo modo, some View no es una forma de escribir un valor de retorno cuyo tipo subyacente pueda variar arbitrariamente en tiempo de ejecución. La implementación elige un tipo subyacente coherente, que puede depender de argumentos genéricos. Un constructor puede codificar las ramas en un resultado compuesto, pero una declaración de resultado opaco por sí sola no unifica tipos de retorno sin relación entre sí. Consulta Tipos de protocolo opacos y en caja.
¿Cuándo deberías borrar el tipo del propio proveedor?
Elige un envoltorio concreto cuando las interfaces posteriores deban recibir un solo tipo de proveedor o cuando quieras que quienes llaman normalicen los proveedores antes de almacenarlos. La siguiente declaración adicional funciona con el protocolo y los proveedores existentes en la muestra. Captura el proveedor y crea su vista cuando se llama a makeView().
@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()
}
}
Para usar esta alternativa, cambia ambas propiedades almacenadas providers del programa completo a [AnyViewProvider]. Sustituye el arreglo inicial de la aplicación por la siguiente expresión:
[
AnyViewProvider(
TitleProvider(id: "title", title: "Component gallery")
),
AnyViewProvider(SwatchProvider(id: "swatch"))
]
Luego, sustituye erasedView(for: provider) en el cierre de ForEach de la galería por provider.makeView(). Como el envoltorio ya devuelve AnyView, esa llamada no necesita ninguna conversión adicional. Esta implementación copia el identificador y captura el valor de proveedor suministrado; no sirve para reemplazar ese valor capturado más adelante.
Si todos los consumidores necesitan una salida con el tipo borrado, un protocolo más sencillo puede exigir directamente makeView() -> AnyView. Esto reduce el código adaptador, pero hace que cada tipo que adopte el protocolo sea responsable del borrado. Elige esta opción cuando la salida con el tipo borrado sea una promesa intencional de la interfaz. Prefiere el contrato con tipo asociado cuando los consumidores genéricos deban seguir aprovechando la relación entre cada proveedor y su salida.
Respeta los límites del compilador y la identidad de las vistas
Mantén separados los recursos del lenguaje y el comportamiento del constructor. La muestra depende de las funciones existenciales de Swift 5.7 y de un resultado explícito AnyView. La documentación actual de Apple distingue entre Xcode 26 y versiones anteriores, y Xcode 27 y posteriores: estas últimas construyen contenido de constructor independiente del tipo y recomiendan ContentBuilder para ese comportamiento. Su documentación sigue describiendo la seguridad de tipos mediante conformidades condicionales. No interpretes la relajación de las comprobaciones de entrada del constructor como una garantía general de que un existencial satisface todas las restricciones genéricas de View. Consulta ViewBuilder y ContentBuilder.
Para un conjunto fijo de componentes, considera un enum cuyos casos contengan datos de componentes y cuya propiedad de renderizado use un switch sobre esos casos. Así, las alternativas admitidas quedan explícitas. Es un diseño útil cuando añadir un componente nuevo debe requerir actualizar un renderizador central; el enfoque con proveedores encaja con interfaces en las que las implementaciones que adoptan el protocolo deben proporcionar su propio renderizado.
El borrado de tipos también tiene una consecuencia documentada sobre la identidad. Apple indica que cambiar el tipo envuelto por un AnyView destruye la jerarquía anterior y crea una nueva. Un identificador estable de la colección no altera ese comportamiento. Si un componente puede alternar entre distintos tipos de vista envueltos, comprueba el comportamiento de su estado y sus transiciones en la aplicación real. Consulta AnyView.
Para esta galería, conserva el contrato del proveedor con tipo asociado, almacena los valores mixtos como existenciales y devuelve AnyView desde una única función auxiliar de renderizado. Traslada el borrado a un envoltorio concreto del proveedor cuando las interfaces posteriores necesiten ese tipo uniforme. Así queda un límite claro que puedes revisar cada vez que cambien la colección, el contrato de renderizado o la configuración del compilador.
Fuentes
- Tipos de protocolo opacos y en caja — The Swift Programming Language
- Tipos de resultado opacos — Swift Evolution
- Desbloquear los existenciales para todos los protocolos — Swift Evolution
- Existenciales abiertos implícitamente — Swift Evolution
- View — Documentación para desarrolladores de Apple
- AnyView — Documentación para desarrolladores de Apple
- init(_:) — Documentación para desarrolladores de Apple
- ForEach — Documentación para desarrolladores de Apple
- ViewBuilder — Documentación para desarrolladores de Apple
- ContentBuilder — Documentación para desarrolladores de Apple