🧠 SwiftUI utiliza estructuras para describir nuestras vistas. Son valores que podemos construir y sustituir con frecuencia. Pero, cuando añadimos varias propiedades @State, aparece una duda razonable: ¿cuánto cuesta conservar ese estado y cómo se compara con una NSView de AppKit?
La respuesta exige mirar qué representa cada pieza, cuánto trabajo realiza y durante cuánto tiempo existe. Contar clases no basta para comparar el coste de dos interfaces.
Una descripción de vista y una NSView cumplen funciones diferentes
Una estructura que implementa View describe una parte de la interfaz para unos datos concretos. Crear un nuevo valor de esa estructura no equivale a reconstruir todos los recursos que muestran el resultado en pantalla.
Una NSView, en cambio, participa directamente en la jerarquía de AppKit y proporciona infraestructura para el dibujado, la disposición y los eventos. Además, una interfaz real puede necesitar subvistas, restricciones y otros recursos. «Una clase y ya está» tampoco refleja necesariamente su coste.
SwiftUI también necesita infraestructura persistente para actualizar y representar la interfaz. Sus estructuras no eliminan ese trabajo: permiten separar la descripción que escribimos de los recursos que administra el framework.
Tampoco debemos convertir struct y class en sinónimos de ligero y pesado. La semántica de valor o referencia describe cómo se comportan los datos; el coste depende también de su contenido, sus asignaciones de memoria y las operaciones que ejecutan.
El estado sobrevive a los valores temporales
🔄 Apple explica en Demystify SwiftUI que los valores de las vistas son temporales, mientras que su identidad conecta las sucesivas actualizaciones. El almacenamiento de @State está asociado a esa identidad.
import SwiftUI
struct ReadingSessionView: View {
let bookTitle: String
@State private var pagesRead = 0
@State private var showsProgress = true
private var progressText: String {
"Páginas leídas: \(pagesRead)"
}
var body: some View {
VStack {
Text(bookTitle)
Toggle("Mostrar progreso", isOn: $showsProgress)
if showsProgress {
Text(progressText)
}
Button("He leído otra página") {
pagesRead += 1
}
}
}
}
SwiftUI puede recibir nuevos valores de ReadingSessionView y conservar las páginas leídas. Si el padre cambia el título y ReadingSessionView conserva su identidad, el contador sigue donde estaba. Si cambia la identidad de ReadingSessionView, por ejemplo al cambiar un .id(...) aplicado desde el padre, SwiftUI inicia una nueva vida para la vista y vuelve a usar los valores iniciales. Ocultar el Text del progreso solo afecta a esa rama. Los dos estados pertenecen a la vista que la contiene.
Además, no todas las propiedades requieren @State. El título llega como entrada y el texto del progreso se calcula a partir de datos existentes. Solo los dos valores que la vista necesita modificar y conservar son estado local.
Qué cambia con Xcode 27
Hasta Xcode 26, @State se implementaba como el property wrapper State<Value>, un struct. En una declaración como @State private var model = Model(), la expresión Model() se evaluaba cada vez que se inicializaba el valor de la vista. SwiftUI seguía conservando la primera instancia para esa identidad, de modo que las siguientes podían crearse y descartarse. Con una clase que realiza trabajo en su inicializador, ahí hay un coste aunque la memoria de esas instancias dure poco. Referencia de State · Nota técnica TN3211.
Al compilar con Xcode 27 o posterior, el atributo usa la macro State(). Apple explica que la expresión inicial se evalúa de forma diferida y que, en el ejemplo anterior, Model() se ejecuta una vez por identidad de la vista. Ese comportamiento está disponible desde iOS 17, macOS 14 y las versiones equivalentes al compilar con la nueva herramienta. El modelo de identidad se mantiene. Lo que cambia es el trabajo que cuesta llegar al valor inicial. What’s new in SwiftUI, WWDC26.
Qué mide el tamaño de un struct
MemoryLayout<T>.size mide los bytes de la representación contigua de T. Un valor puede formar parte de otro valor o vivir dentro de un objeto o del búfer de una colección. También puede contener referencias a memoria externa. Para un tipo de clase como NSView, MemoryLayout<NSView>.size mide la referencia Swift. El objeto, sus subvistas y los recursos que usa quedan fuera de ese número. MemoryLayout.size · Explore Swift performance, WWDC24.
Apple documenta la vida del almacenamiento de @State y cuándo se evalúa el valor inicial. El número de asignaciones, su tamaño y la organización interna quedan fuera de ese contrato público. Por eso, declarar diez estados dice cuántos valores gestiona SwiftUI para esa vista, pero no permite deducir cuántas asignaciones hará en el heap. Demystify SwiftUI.
Cómo comparar las dos implementaciones
Para comparar SwiftUI y AppKit, perfilaría con las herramientas de memoria de Instruments dos pantallas equivalentes en el mismo Mac, con la misma versión de macOS y compiladas en modo Release. Usaría los mismos datos y repetiría la misma secuencia de apertura, interacción y cierre. Con Allocations distinguiría las asignaciones durante la interacción de las que siguen vivas al estabilizarse la pantalla. Con VM Tracker observaría el pico y la huella de memoria del proceso. Ambas cifras responden a preguntas distintas: una implementación puede crear más objetos temporales y conservar menos memoria al final.
En el ejemplo, podríamos reunir pagesRead y showsProgress en un struct y guardarlo en un único @State. La vista seguiría guardando los dos valores. Para saber si agruparlos ahorra memoria, habría que medir ambas versiones.
Al pulsar «He leído otra página», cambia el contador. Con el instrumento SwiftUI de Instruments podemos seguir ese cambio, ver qué vistas se actualizan y detectar si algún body tarda demasiado en evaluarse. Puedes ver ejemplos en la sesión Understanding and improving SwiftUI performance.