🧬 Más allá de `some View`: entendiendo `body` en SwiftUI
Si llevas tiempo escribiendo SwiftUI, hay una pregunta que parece tonta pero rara vez tiene respuesta clara:
“¿Por qué
bodydevuelvesome Viewy noViewa secas?”
Y de paso, otra que va casi siempre con ella: ¿por qué puedo escribir varias vistas, un if/else o un if let dentro de body, si una clausura solo debería devolver un valor?
La respuesta corta es corta. La larga, en cambio, es la que me parece interesante: conecta dos características del lenguaje (opaque types y result builders) y entender cómo encajan cambia el modelo mental con el que escribes vistas.
🧱 El modelo simplificado
Cuando empezamos con SwiftUI, todos hemos escrito esto:
struct ProfileView: View {
var body: some View {
VStack {
Text("Diana")
Image(systemName: "person")
}
}
}
Y nuestro modelo mental es algo así: “body es lo que se dibuja en pantalla”. Funciona, compila, se renderiza. Y la mayoría del tiempo me ha bastado con eso.
Pero hay dos cosas raras ahí: el some delante de View, y el hecho de que dentro del cuerpo puedas escribir lo que parecen “sentencias” (if, if let, varias vistas seguidas) cuando técnicamente eso no debería caber en una sola clausura.
Cada una tiene su explicación, y juntas describen cómo está construido SwiftUI.
🧩 La primera pieza: some View es un opaque type
some View es un tipo opaco. La clave está en una palabra: es un tipo concreto, no cualquier tipo.
Cuando escribes some View, le estás diciendo al compilador:
“Voy a devolver un tipo concreto que conforma
View. Tú averígualo a partir del cuerpo. El que llama a esta propiedad no necesita saber cuál es, pero será siempre el mismo.”
Para SwiftUI esto es esencial. SwiftUI necesita el tipo concreto del árbol de vistas para hacer su trabajo: comparar el árbol antiguo con el nuevo y aplicar solo los cambios que hagan falta (el famoso diffing).
Si body devolviera View directamente (un existential), el tipo se borraría en runtime. SwiftUI tendría que recurrir a comparaciones dinámicas y perdería gran parte de la optimización que ocurre en compilación.
Tres elementos en un VStack se compilan en un único tipo: VStack<TupleView<(Text, Image, Text)>>. Eso es lo que some View está ocultando al que llama.
⚖️ some View vs any View
Esta es la pregunta que suele venir detrás. La diferencia, en corto:
some View: el tipo se decide en compilación. Despacho estático, sin boxing, coste cero. Pero rígido: todas las ramas tienen que devolver el mismo tipo concreto.any View: el tipo se decide en runtime. Despacho dinámico, hay boxing e indirección. Más flexible (permite tipos heterogéneos), pero pagas rendimiento.
SwiftUI usa some precisamente porque el rendimiento del árbol de vistas es donde se la juega.
// ✅ Mismo tipo en ambas ramas
var body: some View {
Text("Hola")
}
// ❌ Esto NO debería compilar: Text e Image son tipos distintos
var body: some View {
if condition {
Text("Hola")
} else {
Image("foto")
}
}
Y de hecho, si lo intentamos en una función normal, sin @ViewBuilder y sin la magia que aplica el protocolo View, el compilador deja muy claro que las dos ramas no son del mismo tipo:
“Function declares an opaque return type ‘some View’, but the return statements in its body do not have matching underlying types”. Es exactamente lo que cabría esperar: some exige un único tipo concreto, y aquí hay dos.
…un momento. Pero entonces, ¿por qué dentro de body sí compila lo mismo?
Ahí entra la segunda pieza.
🪄 La segunda pieza: @ViewBuilder es un result builder
Si miras la definición de body en el protocolo View:
public protocol View {
associatedtype Body: View
@ViewBuilder var body: Self.Body { get }
}
@ViewBuilder es un result builder: una característica del lenguaje que transforma una secuencia de expresiones dentro de una clausura en un único valor compuesto.
Sin @ViewBuilder, una clausura solo puede devolver un valor. Con él, el compilador reescribe el cuerpo:
- Varias vistas seguidas →
TupleView - Un
if/else→_ConditionalContent<TrueView, FalseView> - Un
if letoifsin else → unOptionalenvuelto - Un
switch→ otra forma de_ConditionalContent
Volvamos al ejemplo de antes. Lo que el compilador genera por ti se parece a esto:
var body: some View {
if condition {
return ViewBuilder.buildEither(first: Text("Hola"))
} else {
return ViewBuilder.buildEither(second: Image("foto"))
}
}
// Tipo inferido: _ConditionalContent<Text, Image>
El truco está en que _ConditionalContent<Text, Image> es un único tipo concreto, el mismo en ambas ramas. Por eso some View está contento: a sus ojos body siempre devuelve _ConditionalContent<Text, Image>, sin ambigüedad.
Aunque las ramas devuelvan Text e Image, el body siempre devuelve _ConditionalContent<Text, Image>. Un único tipo, exactamente lo que some View necesita.
@ViewBuilder no es magia. Es el mismo patrón que Swift expone con @resultBuilder para que escribas tus propios DSLs (HTML, SQL, rutas…). Bajo el capó hay funciones declaradas: buildBlock, buildEither(first:), buildEither(second:), buildOptional, buildArray. El compilador las llama por ti.
🔗 Por qué los dos conceptos van juntos
Aquí está la elegancia del diseño:
some Viewexige que siempre devuelvas el mismo tipo concreto.@ViewBuilderse encarga de que, escribas lo que escribas dentro debody, el compilador combine todo en un único tipo concreto compuesto.
Sin @ViewBuilder, some View sería casi imposible de usar en la práctica: cada vez que quisieras un if, tendrías que envolverlo manualmente. Sin some View, SwiftUI no podría hacer su diffing eficiente en compilación.
Los dos juntos dan la API que conocemos: parece que escribes “código normal” dentro de body, pero el compilador está construyendo silenciosamente un tipo monstruosamente específico que SwiftUI luego usa para diffear el árbol.
Si haces print(type(of: contenido)) en una vista mediana, ves el tipo real que el compilador ha construido por ti:
Una vista con VStack, if/else, if let y ForEach anidados produce un tipo compuesto, único y conocido en compilación. Eso es lo que some View está ocultando al que llama, y lo que @ViewBuilder ha compuesto a partir de tu código.
🚪 ¿Y AnyView?
Aparece pronto en cualquier proyecto. La diferencia importante es que some View y AnyView resuelven problemas distintos.
some View mantiene el tipo real oculto al que llama, pero el compilador sí lo conoce. Por ejemplo:
var body: some View {
Text("Hola")
}
Aunque tú ves some View, el compilador sabe perfectamente que eso es un Text.
AnyView, en cambio, es el escape hatch: un type eraser que envuelve cualquier View y oculta su tipo concreto también al compilador.
Lo necesitas cuando quieres devolver tipos genuinamente distintos en ramas distintas (y no puedes refactorizar a _ConditionalContent), o cuando quieres guardar vistas heterogéneas en un Array.
Pero pierdes el diffing fino: SwiftUI no puede comparar dos AnyView estructuralmente. Por eso la regla práctica es evita AnyView salvo que no haya alternativa.
💡 ¿Por qué importa esto en el día a día?
Para mí, lo que más cambia es el modelo mental.
Cuando escribes una vista en SwiftUI no estás escribiendo “código que devuelve UI”. Estás escribiendo una descripción tipada de un árbol de vistas, y el lenguaje colabora contigo para que ese árbol tenga un tipo único, conocido en compilación, que SwiftUI puede comparar y actualizar de forma eficiente.
Eso explica decisiones de diseño que parecían arbitrarias:
- Por qué
AnyViewse desaconseja: rompe el diffing estático. - Por qué
@ViewBuilderaparece en parámetros de tus propias vistas: para que el llamante pueda usar la misma sintaxis que dentro debody. - Por qué a veces el compilador se queja con “the compiler is unable to type-check this expression in reasonable time”: el árbol de tipos compuestos crece tanto que el inferidor de tipos se ahoga. Suele resolverse extrayendo subvistas más pequeñas.
🌱 Lo que me llevo
Después de tirar del hilo me queda esto: body parecía una pieza simple, el sitio donde describes la UI. Pero por debajo se apoyan dos características del lenguaje que rara vez vemos por separado. Opaque return types, para que el compilador conozca el tipo concreto sin exponerlo. Y result builders, para que tú puedas escribir el cuerpo de forma natural y el compilador lo doble en un único valor compuesto.
La próxima vez que me pregunten “¿por qué some View?”, no quiero responder “porque sí”. Quiero contar esta historia: la del compilador que sabe el tipo, la del llamante que no necesita saberlo, y la del result builder que se encarga de que todo encaje.