← Todos los articulos

De qué está hecho SwiftUI

SwiftUI se asienta sobre tres características del lenguaje Swift: result builders, tipos de retorno opacos y un árbol de vistas con tipos por 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 some View vs any View) dejan de ser misteriosas.

Una vista de SwiftUI es un tipo por valor que cumple con un único protocolo con un único 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.

El post recorre el sustrato. Aquí no hay un LiveActivityManager, ni una captura de Get Bananas. El punto es el framework, no un proyecto; una vez que el framework es legible, cada post de código publicado del cluster se lee más limpio.

TL;DR

  • Una vista de SwiftUI es un tipo por valor de Swift que cumple con View. El protocolo tiene un requisito: var body: some View { get }. Todo lo demás está construido sobre características del lenguaje Swift.
  • @ViewBuilder es un result builder. El body de cada View es uno. Los result builders convierten expresiones separadas por comas en un único valor de retorno mediante llamadas sintetizadas por el compilador.
  • some View es un tipo de retorno opaco. El compilador conoce el tipo concreto; quien llama no. El tipo opaco es lo que hace que los bodies de las vistas sean rápidos en compilación y en ejecución; AnyView es la salida de emergencia con borrado de tipo para los casos donde la opacidad no funciona.
  • Group, EmptyView, TupleView, _ConditionalContent son los tipos de implementación que los result builders sintetizan. Están documentados pero rara vez se escriben a mano.

El protocolo que lo inicia todo

El protocolo View tiene un 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 body 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 body, 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 escribes son las ramas.

El atributo @ViewBuilder sobre body. Cada body es una clausura 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 una clausura con una secuencia de expresiones sea transformada 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 de un body de SwiftUI.

La forma del protocolo es inusual por dos razones.

Primero, el requisito es una propiedad computada, no un método. El body de la vista se vuelve a calcular en cada pasada de renderizado cuando SwiftUI decide que el estado de la vista ha cambiado. El framework trata body como barato de invocar; los cálculos largos dentro de body son un anti-patrón porque se ejecutan en cada renderizado.

Segundo, Self.Body es asociado, no borrado. El tipo concreto del body de una vista forma parte de su firma en tiempo de compilación. El tipo del body de Text("Hello") es Never; el tipo del body de una vista personalizada es lo que sea que @ViewBuilder haya sintetizado para el body. El diseño con tipos asociados es lo que permite al compilador optimizar el árbol de vistas sin chequeos 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 una clausura en un único valor de retorno insertando llamadas a métodos sintetizadas por el compilador. @ViewBuilder es un result builder. El body de cada vista de SwiftUI es su clausura.2

Considera esta vista:

struct ExampleView: View {
    var body: some View {
        Text("Title")
        Text("Subtitle")
        Image(systemName: "star")
    }
}

El body tiene tres sentencias sin separador. En Swift normal, eso es un error de compilación: una clausura solo puede devolver un valor. Los result builders reescriben la clausura 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 body devuelve ese único valor de tipo tuple-view. SwiftUI antiguo distribuía un conjunto fijo de sobrecargas de buildBlock para 1, 2, 3, … hasta 10 hijos; SwiftUI actual usa el soporte de genéricos variádicos de Swift (buildBlock<each Content>) por lo que un body con once o más vistas hermanas ya no es un caso especial.

El mismo patrón maneja el flujo de control. Un body de 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 del resultado en tiempo de compilación, aunque solo se ejecute una rama en cualquier renderizado dado.

if let, switch, el desempaquetado opcional y unas pocas construcciones más 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 bucle 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 una RandomAccessCollection y una clausura de contenido y sintetiza la iteración como una única vista con tipo por valor. El DSL no es a medida; son los result builders de Swift, configurados para vistas.

some View y el problema de la opacidad

El body 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 cumple con View, pero no te digo cuál”. El compilador rastrea el tipo concreto internamente para optimización; quien llama a tu vista solo ve el testigo del protocolo. El patrón es lo que permite que el body 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 de un body @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 distintos, y some View requiere uno solo. Los result builders son los que hacen funcionar las formas de retorno condicionales; 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 sobrecoste en tiempo de ejecución). Una función libre o una propiedad puede devolver legalmente any View. Sin embargo, el body del protocolo View no puede: el protocolo requiere associatedtype Body: View, y any View no cumple a su vez con View, por lo que var body: any View no satisface el protocolo y el compilador sugiere some View. La regla práctica: usa some View para los bodies de las vistas, recurre a AnyView (el envoltorio con borrado de tipo) para tipos de vista que varían 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 distintos desde una función some View y necesitas o bien ramificación con result builder o bien AnyView.

AnyView: la salida de emergencia

AnyView es un envoltorio de vista con borrado de tipo. 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 salida de emergencia es una función que devuelve tipos concretos distintos según datos en tiempo de ejecución y que no se puede expresar mediante ramificación con 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 coste 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 renderizados, la jerarquía de vistas existente se destruye y se crea una nueva jerarquía en su lugar, lo que significa estado perdido, animaciones reiniciadas e identidad perdida. Volver a envolver el mismo tipo concreto no desencadena esa destrucción, pero el diffing impulsado por tipos estáticos que el framework prefiere tampoco está disponible de ningún modo.

La regla correcta es: prefiere la ramificación con result builder de @ViewBuilder para vistas condicionales (if, switch, for), prefiere vistas parametrizadas para tipos variables, recurre a AnyView solo cuando ninguno de los 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 una clausura de result builder.

Group, EmptyView, TupleView: los tipos de implementación

El result builder sintetiza tipos de vista concretos específicos. Tres de ellos son útiles de reconocer:6

Group es un contenedor transparente. Acepta hasta diez vistas como contenido y las presenta como hermanas al layout padre. El propio contenedor no añade estructura visual; el contenido se renderiza exactamente como lo haría 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 los result builders hacen 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 body tiene múltiples vistas hermanas. La expresión al inicio de este post 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 clausura puede anotarse con @ViewBuilder, y la misma sintaxis del DSL se vuelve legal dentro de ella.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 cómo VStack, HStack, ZStack, List, Form, Section, Group y NavigationStack propios 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 requerir soporte especial del compilador.

La razón por la que escribes init(@ViewBuilder content:) y no simplemente init(content:) es que el atributo en el parámetro es lo que activa la transformación del result builder dentro del cuerpo de la clausura que pasa quien llama. Sin el atributo, Card { Text("A"); Text("B") } es un error de compilación porque la clausura tiene dos sentencias y no hay @ViewBuilder para transformarlas.

State, bindings y la capa de property wrappers

Todo lo de arriba es 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 la creación de vistas:

@State posee una pieza de estado con tipo por valor dentro de una sola vista. Leer la propiedad lee el almacenamiento subyacente; asignarle dispara un re-renderizado de la vista. El wrapper es apropiado para estado simple, local a la vista (el on/off de un toggle, la cadena en borrador 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 de conformidad antiguo con ObservableObject. La macro aplicada a una clase genera el rastreo del framework Observation para que las propiedades de la clase disparen re-renderizados de la vista cuando se leen dentro de un body y luego cambian. La propiedad del lado de la vista de una clase @Observable pasa de @StateObject a @State simple; las vistas descendentes que necesitan un asidero bidireccional usan @Bindable en lugar de @ObservedObject.

@Environment lee valores inyectados como dependencia desde la cadena de entorno. SwiftUI provee claves de entorno integradas (locale, esquema de color, acción de descarte); las apps añaden claves personalizadas para inyección de dependencias específicas del dominio.

La capa de property wrappers es lo que permite que el body de una vista se vuelva a ejecutar cuando el estado cambia. 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 del árbol de vistas.

Las dos mitades (la capa del árbol de vistas y la capa del estado) están débilmente acopladas. El árbol de vistas tiene tipos por valor y es rápido de recalcular. La capa de estado tiene tipos por referencia (para @Observable) o tipos por valor con puntero al almacenamiento (para @State) y rastrea las lecturas. Juntos producen el modelo del framework de “describe lo que debe estar en 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 del compilador 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 distintos 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 en el body con muchos modificadores agotan el verificador de tipos. Solución: divide el body en propiedades computadas más pequeñas o en sub-vistas; cada pieza que devuelva some View simplifica el trabajo de inferencia.

“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” Una función que esperaba una sola vista recibió el resultado de un body con múltiples sentencias sin @ViewBuilder. Solución: añade @ViewBuilder al parámetro de clausura 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 una clausura vacía. Solución: los result builders necesitan al menos una expresión para inferir Content; las clausuras vacías recurren a EmptyView() si el sitio de llamada lo provee explícitamente.

Los mensajes de error son antipáticos 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 ramificación o AnyView”.

Cuándo recurrir fuera del sustrato

Algunos patrones que el sustrato no maneja limpiamente:

Tipos concretos variádicos. Una función que devuelve un tipo View distinto por rama y que no puedes envolver en ramificación con result builder necesita AnyView. Acepta el coste (diffing perdido, 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 body @ViewBuilder pero limita el conteo de ramas del result builder; los bodies condicionales multi-OS a veces alcanzan el límite de “expresión demasiado compleja”. La solución es extraer las sub-vistas 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 “crear el label, asignar el texto, añadir como subview” no se traslada; el equivalente en SwiftUI es un Text("...") con tipo por valor devuelto desde un body. Los patrones que requieren construcción imperativa suelen ser una 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.

  1. 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.

  2. some View y AnyView resuelven problemas distintos. Los tipos de retorno opacos son el caso por defecto; el borrado de tipo es la salida de emergencia. Recurrir a AnyView debería ser el caso raro; recurrir a some View más ramificación con result builder debería ser el caso común.

  3. Los result builders son todo el DSL. En cualquier sitio donde 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 que VStack es un atributo y un parámetro de clausura.

Empareja este post con la serie de código publicado del cluster: SwiftUI publicando multiplataforma (Return corre en cinco plataformas con un único núcleo SwiftUI compartido); la capa visual de 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 serie de Apple Ecosystem. Para un contexto más amplio de iOS con agentes de IA, mira la guía de iOS Agent Development.

FAQ

¿Qué es el protocolo View en SwiftUI?

El protocolo View tiene un requisito: var body: some View { get }. Cada vista de SwiftUI es un tipo por valor de Swift que cumple con View, con una propiedad computada body que devuelve otra vista (o Never para vistas primitivas como Text, Color, Image, EmptyView). El body está anotado con @ViewBuilder para que pueda usar la sintaxis sin comas del DSL 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 bodies de las vistas devuelvan tipos complejos como VStack<TupleView<(Text, Image, Spacer)>> sin tener que escribirlos, 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 con result builder de @ViewBuilder (if, switch, for) ni los genéricos parametrizados resuelven el problema. Cuando el tipo concreto envuelto cambia entre renderizados, 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 desencadena esa destrucción, pero el diffing impulsado por tipos estáticos que el framework prefiere tampoco está disponible de ningún modo. Si te encuentras recurriendo a AnyView con frecuencia, el patrón que necesita cambiar está aguas arriba: prefiere vistas parametrizadas o empuja la condicional a un body con result builder.

¿Qué es @ViewBuilder y dónde puedo usarlo?

@ViewBuilder es un result builder (característica del lenguaje Swift). Transforma una clausura 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 clausura para dar a quienes llaman 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 cualquier propiedad de estado que el body lee se muta. Los property wrappers (@State, @Binding, @Observable, @Environment) rastrean lecturas y disparan re-renderizados al escribir. Los re-renderizados 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 del árbol.

Referencias


  1. Apple Developer, “View” y “Configuring views”. El protocolo View, el tipo asociado Body y el atributo @ViewBuilder sobre body

  2. Swift Evolution, “SE-0289: Result builders”. La propuesta del lenguaje que formalizó los result builders (introducidos como _functionBuilder en 5.1, formalizados como @resultBuilder en 5.4). Define buildBlock, buildEither, buildOptional, buildArray, buildExpression, buildFinalResult y compañía. 

  3. Apple Developer, “ViewBuilder” y “ForEach”. El tipo de result builder que SwiftUI usa para los bodies de las vistas (buildBlock con genéricos variádicos, buildEither, desempaquetado opcional). ViewBuilder no expone buildArray, por lo que ForEach es el primitivo de iteración para repetir una vista sobre una colección. 

  4. Swift Evolution, “SE-0244: Opaque result types”. La palabra clave some para tipos de retorno opacos, añadida en Swift 5.1. 

  5. Apple Developer, “AnyView”. Envoltorio de vista con borrado de tipo, construcción y la compensación del diffing. 

  6. Apple Developer, “Group”, “EmptyView” y “TupleView”. Tipos de implementación que sintetizan los result builders. 

  7. 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 @Observable de iOS 17+. 

  8. 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:), más la ruta de migración de iOS 17 desde ObservableObject a @Observable

Artículos relacionados

El runtime de watchOS es un contrato, no una tarea en segundo plano

watchOS no tiene un segundo plano al estilo iOS. WKExtendedRuntimeSession es el contrato; sin él, la app se suspende al …

15 min de lectura

Tres superficies: humano, Apple Intelligence, agente

Cada función de una app de iOS enfrenta tres superficies: humano, Apple Intelligence, agente. Cada una tiene obligacione…

16 min de lectura

La capa de limpieza es el verdadero mercado de los agentes de IA

Charlie Labs pasó de construir agentes a limpiar lo que dejan. El mercado de agentes de IA se está moviendo de la genera…

15 min de lectura