Durante años, buena parte de la sintaxis declarativa de SwiftUI se ha apoyado en distintos result builders. ViewBuilder construía jerarquías de vistas, ToolbarContentBuilder hacía lo propio con barras de herramientas y CommandsBuilder resolvía los comandos de una aplicación. El resultado era una API muy expresiva para nosotros, pero bastante más complicada para el compilador de lo que podía parecer a simple vista.
Con Xcode 27, Apple da un paso importante para simplificar ese modelo mediante ContentBuilder, un constructor unificado capaz de construir distintos tipos de contenido sin imponer de entrada que cada expresión cumpla un protocolo concreto. El cambio está orientado principalmente a mejorar el rendimiento de la comprobación de tipos y, aunque en la mayoría de proyectos apenas exige modificar código, sí cambia de forma relevante la arquitectura interna con la que SwiftUI interpreta nuestros bloques declarativos.
Antes de ContentBuilder: varios builders para problemas muy parecidos
Cuando escribimos una vista como esta:
struct DashboardView: View {
var body: some View {
VStack {
Text("Resumen")
Image(systemName: "chart.bar.fill")
Button("Actualizar") {
refresh()
}
}
}
private func refresh() {
// ...
}
}
el compilador no interpreta las tres expresiones del VStack como simples sentencias independientes. El parámetro content de este tipo de contenedores está asociado a un result builder, tradicionalmente ViewBuilder, y Swift transforma internamente ese bloque para producir un único valor que representa todo el contenido.
De forma muy simplificada, un builder propio podría comportarse así:
@resultBuilder
enum MessageBuilder {
static func buildBlock(_ components: String...) -> [String] {
components
}
}
func makeMessages(
@MessageBuilder content: () -> [String]
) -> [String] {
content()
}
let messages = makeMessages {
"Sincronizando datos"
"Procesando cambios"
"Operación completada"
}
El código parece contener tres String, pero el builder los combina para devolver un único [String].
SwiftUI lleva este patrón mucho más lejos. Sus constructores permiten manejar expresiones diferentes, condicionales, disponibilidad de APIs y estructuras compuestas sin obligarnos a escribir manualmente el tipo resultante.
El problema no era la sintaxis, sino la inferencia de tipos
La dificultad aparece cuando una misma pieza de SwiftUI puede participar en distintos contextos. Tipos como Group, Section o ForEach han tenido que ofrecer durante años distintas variantes de sus inicializadores para trabajar con constructores especializados.
Por ejemplo, conceptualmente podían existir contextos equivalentes a estos:
@ViewBuilder
var content: some View {
Group {
Text("Perfil")
Text("Configuración")
}
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
Group {
ToolbarItem {
Button("Guardar") {
save()
}
}
}
}
Aunque para el desarrollador la intención está bastante clara, el compilador necesita determinar qué sobrecarga de cada elemento encaja con el contexto exterior. Si dentro del bloque aparecen más Group, Section o ForEach, el número de posibilidades que el type checker debe comprobar aumenta rápidamente.
Esta es una de las razones por las que ciertas vistas SwiftUI muy grandes han acabado históricamente provocando tiempos de compilación elevados o el conocido (y temido) error:
the compiler is unable to type-check this expression in reasonable time
Separar una vista compleja en propiedades o subviews podía aliviar el problema porque reducía el tamaño de cada expresión que Swift debía resolver. Sin embargo, eso atacaba el síntoma desde nuestro código, no la causa estructural dentro de SwiftUI.
Qué cambia con ContentBuilder
ContentBuilder unifica gran parte de esos builders especializados bajo un mismo mecanismo. En la API actual aparece como un alias de ViewBuilder, pero con Xcode 27 SwiftUI utiliza esa infraestructura para construir contenido de forma independiente del protocolo que finalmente necesite el contexto.
En lugar de exigir durante la construcción algo conceptualmente parecido a:
Content: View
el builder puede formar primero una estructura de contenido genérica y dejar que sea el tipo resultante quien determine después si cumple los requisitos necesarios.
Esto permite que el mismo modelo pueda servir para vistas, elementos de unA toolbar u otros tipos de contenido de SwiftUI.
@ContentBuilder
var mainContent: some View {
Text("Actividad")
ProgressView(value: 0.72)
}
También puede utilizarse en un contexto diferente:
@ContentBuilder
var editingToolbar: some ToolbarContent {
ToolbarItem {
Button("Duplicar", systemImage: "plus.square.on.square") {
duplicateSelection()
}
}
ToolbarItem {
Button("Eliminar", systemImage: "trash") {
deleteSelection()
}
}
}
La notación es la misma, pero el tipo que exige el contexto final es distinto.
La clave está en las conformidades condicionales
La unificación no significa que SwiftUI haya renunciado a la seguridad de tipos. ContentBuilder no acepta cualquier combinación de contenido para convertirla mágicamente en cualquier otra cosa.
Lo que cambia es el momento en el que se comprueba la conformidad.
El builder puede generar un contenedor compuesto sin exigir inicialmente que sus elementos sean View, ToolbarContent o cualquier otro protocolo específico. Después, ese tipo compuesto obtiene la conformidad apropiada únicamente cuando todos sus elementos pueden cumplirla.
Podemos imaginar un modelo simplificado como este:
struct PairContent<First, Second> {
let first: First
let second: Second
}
protocol Renderable {
func render()
}
extension PairContent: Renderable
where First: Renderable, Second: Renderable {
func render() {
first.render()
second.render()
}
}
PairContent puede contener dos tipos arbitrarios. Solo se convierte en Renderable cuando tanto First como Second también lo son.
Ese enfoque es muy parecido a la idea que permite a SwiftUI construir primero el contenido y verificar después si encaja con el protocolo solicitado por el contexto.
Por qué mejora los tiempos de compilación
La consecuencia más interesante para el desarrollo diario es que Swift tiene que explorar muchas menos alternativas.
Antes, si un Group tenía varios inicializadores similares asociados a builders diferentes, el compilador podía necesitar analizar el bloque completo para descubrir cuál era el correcto. Si dentro había otro componente igualmente sobrecargado, el trabajo volvía a repetirse teniendo en cuenta las decisiones anteriores.
Con un builder común, muchas de esas rutas desaparecen. Group, ForEach o Section pueden apoyarse en una única familia de inicializadores y dejar que las conformidades condicionales resuelvan posteriormente qué clase de contenido se ha construido.
La sintaxis del proyecto puede permanecer prácticamente idéntica, pero el compilador tiene bastante menos trabajo que hacer.
Section("Dispositivos") {
ForEach(devices) { device in
Group {
Text(device.name)
if device.isOnline {
Label("Conectado", systemImage: "wifi")
}
}
}
}
Este tipo de jerarquías anidadas es precisamente donde una reducción del número de sobrecargas candidatas puede tener un impacto considerable.
No depende del deployment target
Otro detalle especialmente interesante es que la mejora está ligada a la versión de Xcode con la que compilamos, no únicamente a la versión del sistema operativo en el que ejecutará la aplicación.
Por tanto, un proyecto que mantenga compatibilidad con versiones anteriores de iOS puede beneficiarse del nuevo comportamiento de ContentBuilder simplemente al compilar con Xcode 27.
Eso lo diferencia de muchas novedades de SwiftUI, donde adoptar una API reciente obliga a envolver el código en comprobaciones como:
if #available(iOS 27, *) {
// API nueva
} else {
// Compatibilidad
}
ContentBuilder es una evolución de la infraestructura existente de ViewBuilder, de modo que su principal beneficio se obtiene durante la compilación y no requiere elevar el sistema mínimo soportado de la aplicación.
¿Hay que sustituir ViewBuilder por ContentBuilder?
No de forma inmediata.
ViewBuilder sigue existiendo y, en Xcode 27, SwiftUI utiliza el nuevo modelo también cuando encuentra bloques marcados con @ViewBuilder. De hecho, ContentBuilder se expone como un alias relacionado directamente con ViewBuilder.
Este código sigue siendo perfectamente válido:
struct Card<Content: View>: View {
private let content: Content
init(
@ViewBuilder content: () -> Content
) {
self.content = content()
}
var body: some View {
content
.padding()
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 16))
}
}
Pero si estamos diseñando una API nueva y queremos expresar que el bloque construye contenido de SwiftUI sin asociarlo conceptualmente solo a View, @ContentBuilder comunica mejor esa intención.
struct Panel<Content: View>: View {
private let content: Content
init(
@ContentBuilder content: () -> Content
) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
content
}
.padding()
}
}
En este ejemplo el resultado sigue siendo Content: View, porque Panel necesita mostrarlo dentro de su body. Lo que cambia es que la construcción del bloque utiliza la infraestructura unificada.
ContentBuilder no sustituye a @resultBuilder
También es importante distinguir la novedad de SwiftUI de la característica del lenguaje sobre la que se apoya.
@resultBuilder continúa siendo la herramienta general de Swift para crear DSLs declarativos. Podemos seguir definiendo nuestros propios builders independientemente de SwiftUI.
@resultBuilder
enum QueryBuilder {
static func buildExpression(_ expression: String) -> [String] {
[expression]
}
static func buildBlock(_ components: [String]...) -> [String] {
components.flatMap { $0 }
}
static func buildOptional(_ component: [String]?) -> [String] {
component ?? []
}
}
func buildQuery(
@QueryBuilder filters: () -> [String]
) -> String {
filters().joined(separator: " AND ")
}
let includeArchived = false
let query = buildQuery {
"status = 'active'"
"priority >= 3"
if includeArchived {
"archived = true"
}
}
Aquí buildExpression transforma cada expresión individual, buildBlock combina los resultados y buildOptional permite utilizar un if sin else.
Swift dispone además de métodos como buildEither(first:), buildEither(second:), buildArray(_:), buildLimitedAvailability(_:) o buildFinalResult(_:) para soportar otras construcciones del lenguaje.
buildPartialBlock y la evolución de los result builders
Los constructores modernos ni siquiera tienen que depender exclusivamente de un buildBlock con todos los componentes a la vez. Swift también permite utilizar:
static func buildPartialBlock(first: Component) -> Component
static func buildPartialBlock(
accumulated: Component,
next: Component
) -> Component
Con ellos, el resultado puede construirse progresivamente. Esto reduce la necesidad de declarar múltiples sobrecargas genéricas para distintas cantidades de elementos.
Un ejemplo sencillo sería:
@resultBuilder
enum PathBuilder {
static func buildPartialBlock(first: String) -> String {
first
}
static func buildPartialBlock(
accumulated: String,
next: String
) -> String {
accumulated + "/" + next
}
}
@PathBuilder
func assetPath() -> String {
"images"
"avatars"
"profile.png"
}
El compilador puede construir el resultado como una sucesión de pasos en lugar de necesitar una función distinta capaz de recibir exactamente tres componentes.
Aunque esta característica pertenece a Swift y no específicamente a ContentBuilder, ayuda a entender por qué los result builders son mucho más que syntactic sugar: forman parte activa del sistema de inferencia de tipos y su diseño tiene consecuencias directas sobre el rendimiento del compilador.
Un cambio interno con efectos muy visibles
ContentBuilder es una de esas novedades que probablemente pase desapercibida al ejecutar una aplicación. No añade un nuevo control, no cambia el aspecto de la interfaz y no obliga a reescribir nuestras vistas.
Sin embargo, ataca uno de los puntos de fricción históricos de SwiftUI: el coste que supone para el compilador resolver grandes jerarquías declarativas llenas de genéricos y sobrecargas.
Al unificar builders, separar la construcción del contenido de las restricciones específicas de cada protocolo y apoyarse en conformidades condicionales, SwiftUI consigue mantener su seguridad de tipos reduciendo al mismo tiempo el trabajo que debe realizar la comprobación de tipos.
Para quienes mantenemos proyectos grandes, esa clase de cambio puede resultar más importante que muchas APIs visibles: menos tiempo esperando a que compile una vista compleja, menos expresiones que necesitan dividirse únicamente para ayudar al compilador y una base más sencilla sobre la que Apple puede seguir ampliando SwiftUI.