De qué está hecho SwiftUI
Género: explicación de framework. Esta entrada explica el sustrato sobre el que se asienta SwiftUI: result builders, tipos de retorno opacos y un árbol de vistas con tipos de valor. Una vez visible el sustrato, las partes de SwiftUI que sorprenden a los desarrolladores (AnyView, Group, ViewBuilder, los parámetros @ViewBuilder, el temido error de some View vs any View) dejan de ser misteriosas.
Una vista en SwiftUI es un tipo de valor que conforma un solo protocolo con un solo requisito. El resto del framework está construido sobre características del lenguaje Swift que existen fuera de SwiftUI: result builders, tipos opacos, genéricos con restricciones, property wrappers. Si entiendes las características del lenguaje, el framework se lee como API Swift normal. Si no, el framework se lee como magia que ocasionalmente muerde.
La entrada recorre el sustrato. Aquí no hay ningún LiveActivityManager, ni captura de pantalla de Get Bananas. El punto es el framework, no un proyecto; una vez que el framework es legible, cada entrada de código publicado del clúster se lee con más claridad.
TL;DR
- Una vista de SwiftUI es un tipo de valor de Swift que conforma
View. El protocolo tiene un solo requisito:var body: some View { get }. Todo lo demás está construido sobre características del lenguaje Swift. @ViewBuilderes un result builder. El cuerpo de cadaViewlo es. Los result builders convierten expresiones separadas por comas en un único valor de retorno mediante llamadas sintetizadas por el compilador.some Viewes un tipo de retorno opaco. El compilador conoce el tipo concreto; quien llama, no. El tipo opaco es lo que hace que los cuerpos de vistas sean rápidos en compilación y en tiempo de ejecución;AnyViewes la vía de escape de borrado de tipos para los casos en que la opacidad no funciona.Group,EmptyView,TupleView,_ConditionalContentson los tipos de implementación que sintetizan los result builders. Están documentados, pero rara vez se escriben a mano.
El protocolo del que parte todo
El protocolo View tiene un solo requisito:1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
Dos partes de ese protocolo importan para entender el resto de SwiftUI.
El tipo asociado Body : View. El cuerpo de una vista es a su vez una vista. La recursión es lo que hace al framework componible. Cada View devuelve otra View desde su cuerpo, y así sucesivamente, hasta que la cadena termina en una de las vistas primitivas del framework (como Text, Color, Image, EmptyView) cuyo Body es Never. Las vistas primitivas son las hojas del árbol; las vistas que tú escribes son las ramas.
El atributo @ViewBuilder sobre body. Cada cuerpo es un closure de result builder. Los result builders son una característica del lenguaje Swift documentada en SE-0289 (formalizada como @resultBuilder en Swift 5.4) que permite que un closure con una secuencia de expresiones sea transformado por el compilador en un único valor de retorno mediante llamadas a métodos sintetizados.2 La transformación es lo que hace funcionar la sintaxis sin comas, con forma de sentencias, dentro del cuerpo de SwiftUI.
La forma del protocolo es inusual por dos razones.
Primero, el requisito es una propiedad computada, no un método. El cuerpo de la vista se vuelve a calcular en cada pasada de render cuando SwiftUI decide que el estado de la vista ha cambiado. El framework trata body como algo barato de invocar; los cálculos largos dentro de body son un antipatrón porque se ejecutan en cada render.
Segundo, Self.Body es asociado, no borrado. El tipo concreto del cuerpo de una vista forma parte de su firma en tiempo de compilación. El tipo del cuerpo de Text("Hello") es Never; el tipo del cuerpo de una vista personalizada es lo que @ViewBuilder haya sintetizado para ese cuerpo. El diseño con tipos asociados es lo que permite al compilador optimizar el árbol de vistas sin verificaciones de tipo en tiempo de ejecución. También es lo que crea el requisito de some View cuando una vista personalizada devuelve contenido condicional.
Result Builders: el DSL sin comas
Un result builder es una característica del lenguaje Swift que transforma un closure en un único valor de retorno insertando llamadas a métodos sintetizadas por el compilador. @ViewBuilder es un result builder. El cuerpo de cada vista de SwiftUI es su closure.2
Considera esta vista:
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
El cuerpo tiene tres sentencias sin separador. En Swift normal, eso es un error de compilación: un closure solo puede devolver un valor. Los result builders reescriben el closure antes de la compilación. El código real que ve el compilador, después de la expansión de @ViewBuilder, es aproximadamente:
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:) es un método estático que toma tres vistas y devuelve un TupleView<(Text, Text, Image)>. El cuerpo devuelve ese único valor de tipo tupla-vista. SwiftUI más antiguo incluía un conjunto fijo de sobrecargas de buildBlock para 1, 2, 3, … hasta 10 hijos; SwiftUI actual usa el soporte de Swift para genéricos variádicos (buildBlock<each Content>), de modo que un cuerpo con once o más vistas hermanas ya no es un caso especial.
El mismo patrón maneja el control de flujo. El cuerpo de una vista con una sentencia if se ve así:
struct ConditionalView: View {
let isActive: Bool
var body: some View {
if isActive {
Text("Active")
} else {
Text("Inactive")
}
}
}
@ViewBuilder lo reescribe mediante llamadas a buildEither(first:) / buildEither(second:), produciendo un _ConditionalContent<Text, Text>. El compilador conoce el tipo de resultado en tiempo de compilación, aunque solo se ejecute una rama en cada render.
if let, switch, el desempaquetado de opcionales y otras pocas construcciones son manejados por los distintos métodos estáticos buildXxx del result builder.3 El contenido repetido es el caso notable que la característica del lenguaje sí soporta mediante buildArray, pero @ViewBuilder no: un for crudo dentro de un body falla con “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” La respuesta con forma de SwiftUI es ForEach, que toma un RandomAccessCollection y un closure de contenido y sintetiza la iteración como una única vista de tipo de valor. El DSL no es a medida; es result builders de Swift, configurados para vistas.
some View y el problema de la opacidad
El cuerpo de una vista personalizada normalmente devuelve some View. La palabra clave es tipo de retorno opaco y se añadió en Swift 5.1.4
some View dice: “Devuelvo un tipo específico que conforma View, pero no te digo cuál”. El compilador rastrea el tipo concreto internamente para la optimización; quien llama a tu vista solo ve el testigo del protocolo. El patrón es lo que permite que el cuerpo de una vista devuelva un tipo complejo como VStack<TupleView<(Text, Image, Spacer)>> sin requerir que escribas ese tipo en tu código fuente.
Dos cosas sobre some View que confunden a los desarrolladores nuevos en SwiftUI:
some View es un tipo específico, incluso cuando devuelves cosas distintas. La expresión if condition { Text("A") } else { Image("b") } está permitida dentro del cuerpo de un @ViewBuilder porque el result builder envuelve ambas ramas en _ConditionalContent, produciendo un único tipo concreto. Pero la expresión if condition { return Text("A") } else { return Image("b") } fuera de un result builder es un error de compilación: las dos ramas devuelven tipos concretos diferentes, y some View requiere uno solo. Los result builders son lo que hacen funcionar las formas de retorno condicional; los return explícitos pierden la transformación del result builder.
some View no es lo mismo que any View. some View es opaco (un tipo específico, oculto); any View es existencial (una caja que puede contener cualquier tipo conforme, con sobrecarga en tiempo de ejecución). Una función o propiedad libre puede legalmente devolver any View. Sin embargo, el body del protocolo View no puede: el protocolo requiere associatedtype Body: View, y any View no se conforma a sí mismo a View, así que var body: any View no satisface el protocolo y el compilador sugiere some View. La regla práctica: usa some View para los cuerpos de vista, recurre a AnyView (el envoltorio con borrado de tipos) para tipos de vista variantes en tiempo de ejecución. El mensaje de error “function declares an opaque return type but the return statements in its body do not have matching underlying types” casi siempre significa que intentaste devolver tipos concretos diferentes desde una función some View y necesitas o bien ramificación de result builder o bien AnyView.
AnyView: la vía de escape
AnyView es un envoltorio de vista con borrado de tipos. Su construcción es AnyView(myView). El envoltorio contiene cualquier vista conforme, y SwiftUI lo acepta donde se espera una View.5
El caso de uso de vía de escape es una función que devuelve diferentes tipos concretos según datos en tiempo de ejecución y que no puede expresarse mediante ramificación de result builder:
func viewForKind(_ kind: Kind) -> AnyView {
switch kind {
case .text: return AnyView(Text("hello"))
case .image: return AnyView(Image("photo"))
case .custom: return AnyView(MyCustomView())
}
}
El costo de AnyView es que el tipo subyacente no forma parte de la identidad estática de la vista. La documentación de Apple describe la consecuencia directamente: cuando el tipo envuelto dentro de un AnyView cambia entre renders, la jerarquía de vistas existente se destruye y se crea una nueva en su lugar, lo que significa pérdida de estado, animaciones reiniciadas e identidad perdida. Volver a envolver el mismo tipo concreto no dispara esa destrucción, pero el diffing basado en tipo estático que prefiere el framework tampoco está disponible de cualquiera de las dos maneras.
La regla correcta es: prefiere la ramificación de result builder de @ViewBuilder para vistas condicionales (if, switch, for), prefiere vistas parametrizadas para tipos variables, recurre a AnyView solo cuando ninguna de las dos funcione. Una func viewForKind que devuelve AnyView suele ser una señal de que deberías hacer que viewForKind devuelva some View y poner un switch dentro de un closure de result builder.
Group, EmptyView, TupleView: los tipos de implementación
El result builder sintetiza tipos de vista concretos específicos. Tres de ellos vale la pena reconocer:6
Group es un contenedor transparente. Acepta hasta diez vistas como contenido y las presenta como hermanas para el layout padre. El contenedor en sí no añade estructura visual; los contenidos se renderizan exactamente como lo harían individualmente. El caso de uso es envolver múltiples vistas en un contexto que espera una sola vista (un modificador .if, un retorno condicional, una función que produce “una vista”). Group { Text("A"); Text("B") } es una sola vista que contiene dos; es la forma explícita de lo que hacen los result builders implícitamente.
EmptyView es una vista que no renderiza nada. El result builder la usa como rama falsa condicional cuando un if no tiene else. Devolver EmptyView() desde tu propio código es una manera de optar por no renderizar sin cambiar el tipo de retorno de la función.
TupleView es el tipo concreto que producen los result builders cuando un cuerpo tiene múltiples vistas hermanas. La expresión al inicio de esta entrada que devuelve tres vistas hermanas en realidad devuelve un TupleView<(Text, Text, Image)>. Casi nunca escribes TupleView directamente; lo lees en mensajes de error.
_ConditionalContent (con el guion bajo inicial) es el tipo que maneja las ramas if/else. El tipo aparece en la superficie pública de ViewBuilder, pero el nombre con guion bajo señala “no escribas contra esto a la ligera”; deja que el result builder lo sintetice a partir de if/else en lugar de construirlo a mano.
@ViewBuilder en tus propias funciones
Los result builders no son solo para body. Cualquier función o parámetro de closure puede anotarse con @ViewBuilder, y la misma sintaxis del DSL pasa a ser legal dentro de él.2
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage: callers get the result-builder DSL inside the closure.
Card {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
El patrón es la forma en que los propios VStack, HStack, ZStack, List, Form, Section, Group y NavigationStack de SwiftUI aceptan múltiples hijos. Cada uno de esos tipos toma un parámetro @ViewBuilder content: () -> Content. Reconocer el patrón significa que puedes escribir tus propias vistas contenedoras con la misma ergonomía que las del framework, sin necesidad de soporte especial del compilador.
La razón por la que escribes init(@ViewBuilder content:) y no solo init(content:) es que el atributo en el parámetro es lo que activa la transformación del result builder dentro del cuerpo del closure que pasa quien llama. Sin el atributo, Card { Text("A"); Text("B") } es un error de compilación porque el closure tiene dos sentencias y no hay @ViewBuilder que las transforme.
Estado, bindings y la capa de property wrappers
Todo lo de arriba trata sobre la forma del árbol de vistas. La otra mitad de SwiftUI es el estado, y esa mitad está construida sobre los property wrappers de Swift.7
Los property wrappers más relevantes para escribir vistas:
@State posee una pieza de estado de tipo de valor dentro de una sola vista. Leer la propiedad lee el almacenamiento subyacente; asignarle un valor dispara un re-render de la vista. El wrapper es apropiado para estado simple, local a la vista (el on/off de un toggle, el borrador de cadena de un campo de texto).
@Binding es una referencia bidireccional al estado de otra vista. Una vista hija que necesita leer y escribir el estado de un padre toma un parámetro Binding<T>. El padre construye el binding mediante $state (la proyección con signo de dólar sobre @State).
@Observable (iOS 17+) es una macro que reemplaza el patrón anterior de conformidad a ObservableObject. La macro aplicada a una clase genera el rastreo del framework de Observation para que las propiedades de la clase disparen re-renders de la vista cuando se leen dentro de un body y luego se modifican. La propiedad de un @Observable por parte de la vista pasa de @StateObject a @State simple; las vistas descendentes que necesitan un asa bidireccional usan @Bindable en lugar de @ObservedObject.
@Environment lee valores inyectados como dependencia de la cadena de entorno. SwiftUI proporciona claves de entorno integradas (locale, esquema de color, acción de descartar); las apps añaden claves personalizadas para inyección de dependencias específica del dominio.
La capa de property wrappers es lo que permite al body de una vista volver a ejecutarse cuando cambia el estado. SwiftUI rastrea las lecturas dentro de body mediante dos mecanismos distintos: AttributeGraph (el grafo de dependencias privado de Apple que respalda @State, @Binding y @Environment) para la ruta más antigua de property wrappers, y el framework Observation de la biblioteca estándar (withObservationTracking, público en iOS 17+) para los tipos @Observable.8 Cuando una propiedad rastreada se muta, los bodies correspondientes se vuelven a ejecutar y la maquinaria de diffing calcula el cambio mínimo en el árbol de vistas.
Las dos mitades (la capa del árbol de vistas y la capa de estado) están débilmente acopladas. El árbol de vistas tiene tipos de valor y es rápido de recalcular. La capa de estado es de tipos de referencia (para @Observable) o de tipo de valor con puntero a almacenamiento (para @State) y rastrea las lecturas. Juntas producen el modelo del framework de “describe lo que debería estar en la pantalla como una función del estado, y el framework calcula el diff”.
Lo que ahora reconoces en los mensajes de error
Leer los errores de compilación de SwiftUI con el sustrato visible:
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” Dos sentencias return con tipos concretos diferentes en una función some View. Solución: usa @ViewBuilder para que el result builder envuelva ambas en _ConditionalContent, o envuelve ambos retornos en AnyView.
“The compiler is unable to type-check this expression in reasonable time.” Cadenas largas de body con muchos modificadores agotan el verificador de tipos. Solución: divide el body en propiedades computadas más pequeñas o subvistas; cada pieza que devuelve some View simplifica el trabajo de inferencia.
“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” Una función que esperaba una vista recibió el resultado de un body con múltiples sentencias sin @ViewBuilder. Solución: añade @ViewBuilder al parámetro de closure que acepta el contenido con múltiples sentencias.
“Generic parameter ‘Content’ could not be inferred.” Un contenedor personalizado toma @ViewBuilder content: () -> Content y el sitio de llamada tiene un closure vacío. Solución: los result builders necesitan al menos una expresión para inferir Content; los closures vacíos recurren a EmptyView() si el sitio de llamada lo proporciona explícitamente.
Los mensajes de error son poco amistosos porque el sustrato es invisible. Leerlos con el sustrato visible convierte la mayoría en “ah, el result builder no puede transformar esto” o “ah, necesito o ramificación o AnyView”.
Cuándo recurrir a algo fuera del sustrato
Algunos patrones que el sustrato no maneja con limpieza:
Tipos concretos variádicos. Una función que devuelve un tipo de View distinto por rama y que no puedes envolver en ramificación de result builder necesita AnyView. Acepta el costo (pérdida de diffing, sin animación) y documenta el sitio de llamada.
Vistas condicionales multiplataforma. El #if os(iOS) en tiempo de compilación funciona dentro de un cuerpo @ViewBuilder, pero limita el conteo de ramas del result builder; los bodies condicionales para múltiples sistemas operativos a veces alcanzan el límite de “expression too complex”. La solución es extraer subvistas por plataforma a funciones separadas, cada una devolviendo some View.
Construcción imperativa de vistas. El framework espera que las vistas sean expresiones, no objetos construidos-y-luego-mutados. El estilo UIKit de “crea la etiqueta, asigna el texto, añádela como subvista” no se traduce; el equivalente en SwiftUI es un Text("...") con tipo de valor devuelto desde un body. Los patrones que requieren construcción imperativa suelen ser señal de que el trabajo pertenece a un puente UIViewRepresentable hacia UIKit.
Lo que el patrón significa para apps que se publican en iOS 26+
Tres conclusiones.
-
SwiftUI es Swift, no magia. Los result builders, los tipos de retorno opacos y los property wrappers están todos en la referencia del lenguaje Swift. Leer el framework como código Swift, no como un DSL especial, hace predecibles las partes sorprendentes.
-
some ViewyAnyViewresuelven problemas distintos. Los tipos de retorno opacos son lo predeterminado; el borrado de tipos es la vía de escape. Recurrir aAnyViewdebería ser el caso raro; recurrir asome Viewcon ramificación de result builder debería ser el común. -
Los result builders son el DSL completo. Donde sea que una función o parámetro sea
@ViewBuilder, la sintaxis sin comas, con forma de sentencias, está disponible. Escribir tus propias vistas contenedoras con la misma ergonomía queVStackes un atributo y un parámetro de closure.
Empareja esta entrada con la serie de código publicado del clúster: la publicación de SwiftUI multiplataforma (Return corre en cinco plataformas con un único núcleo SwiftUI compartido); la capa visual Liquid Glass; la máquina de estados de Live Activities en iOS; el contrato del runtime de watchOS en Apple Watch. El hub está en la Apple Ecosystem Series. Para un contexto más amplio sobre iOS con agentes de IA, consulta la iOS Agent Development guide.
Preguntas frecuentes
¿Qué es el protocolo View en SwiftUI?
El protocolo View tiene un solo requisito: var body: some View { get }. Cada vista de SwiftUI es un tipo de valor de Swift que conforma View, con una propiedad computada body que devuelve otra vista (o Never para vistas primitivas como Text, Color, Image, EmptyView). El body se anota con @ViewBuilder para que pueda usar la sintaxis del DSL sin comas de SwiftUI.
¿Qué significa some View?
some View es un tipo de retorno opaco (Swift 5.1+). El compilador conoce el tipo concreto; quien llama solo ve el testigo del protocolo. Los tipos opacos permiten que los cuerpos de vista devuelvan tipos complejos como VStack<TupleView<(Text, Image, Spacer)>> sin escribirlos explícitamente, mientras se preserva la optimización en tiempo de compilación. some View es un tipo específico, aunque ese tipo no sea visible en el sitio de llamada.
¿Cuándo debo usar AnyView?
Usa AnyView solo cuando ni la ramificación de result builder de @ViewBuilder (if, switch, for) ni los genéricos parametrizados resuelven el problema. Cuando el tipo concreto envuelto cambia entre renders, la jerarquía de vistas existente se destruye y se crea una nueva en su lugar; ese es el momento en que las animaciones se reinician y el estado de la vista se restablece. Volver a envolver el mismo tipo concreto no dispara esa destrucción, pero el diffing basado en tipo estático que prefiere el framework tampoco está disponible de cualquiera de las dos maneras. Si te encuentras recurriendo a AnyView con frecuencia, el patrón que necesita cambiar está aguas arriba: prefiere vistas parametrizadas o lleva la condición a un body de result builder.
¿Qué es @ViewBuilder y dónde puedo usarlo?
@ViewBuilder es un result builder (característica del lenguaje Swift). Transforma un closure con múltiples expresiones en un único valor de retorno insertando llamadas sintetizadas por el compilador a buildBlock, buildEither, buildOptional, etc. El body de cada vista de SwiftUI es @ViewBuilder por defecto. Puedes aplicar @ViewBuilder a cualquier función o parámetro de closure para dar a quien llama la misma sintaxis del DSL; VStack, Card y Section usan el mismo patrón para aceptar múltiples hijos.
¿Por qué se vuelve a renderizar el body de mi vista cuando no lo esperaba?
SwiftUI vuelve a ejecutar body cada vez que se muta cualquier propiedad de estado que el body lee. Los property wrappers (@State, @Binding, @Observable, @Environment) rastrean las lecturas y disparan re-renders en las escrituras. Los re-renders inesperados suelen rastrearse a un cambio en el estado de una vista padre, un cambio en un valor de entorno o la modificación de una propiedad leída de un objeto @Observable. El diffing del framework calcula entonces el cambio mínimo en el árbol.
Referencias
-
Apple Developer, “View” y “Configuring views”. El protocolo
View, el tipo asociadoBodyy el atributo@ViewBuildersobrebody. ↩ -
Swift Evolution, “SE-0289: Result builders”. La propuesta del lenguaje que formalizó los result builders (introducidos como
_functionBuilderen 5.1, formalizados como@resultBuilderen 5.4). DefinebuildBlock,buildEither,buildOptional,buildArray,buildExpression,buildFinalResulty compañía. ↩↩↩ -
Apple Developer, “ViewBuilder” y “ForEach”. El tipo de result builder que SwiftUI usa para los cuerpos de vista (
buildBlockcon genéricos variádicos,buildEither, desempaquetado de opcionales).ViewBuilderno exponebuildArray, así queForEaches la primitiva de iteración para repetir una vista sobre una colección. ↩ -
Swift Evolution, “SE-0244: Opaque result types”. La palabra clave
somepara tipos de retorno opacos, añadida en Swift 5.1. ↩ -
Apple Developer, “AnyView”. Envoltorio de vista con borrado de tipos, construcción y la concesión en el diffing. ↩
-
Apple Developer, “Group”, “EmptyView” y “TupleView”. Tipos de implementación que sintetizan los result builders. ↩
-
Apple Developer, “State and Data Flow”. La capa de property wrappers:
@State,@Binding,@Observable,@Environment. El sistema de observación de SwiftUI y la macro@Observablede iOS 17+. ↩ -
Apple Developer, “Observation” y “Migrating from the Observable Object protocol to the Observable macro”. El framework Observation de la biblioteca estándar, incluyendo
withObservationTracking(_:onChange:), además de la ruta de migración en iOS 17 deObservableObjecta@Observable. ↩