
SwiftUI introduce una forma declarativa de construir interfaces de usuario, donde la administración de estado juega un papel fundamental para garantizar una actualización eficiente y precisa de las vistas. Sin embargo, el uso incorrecto de las herramientas de administración de estado puede llevar a errores difíciles de diagnosticar, pérdida de datos o comportamientos inesperados.
En este artículo, explicaremos cómo y cuándo usar las herramientas de administración de estado de SwiftUI: @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, con ejemplos claros de uso correcto e incorrecto.
Pero primero es necesario comprender la topología y funcionamiento interno de SwiftUI y cómo este maneja la reactividad del estado.
Fundamentos de SwiftUI
SwiftUI es un marco declarativo para el desarrollo de interfaces gráficas (UI). Decimos declarativo porque el diseño se basa en especificaciones de cómo debería verse y funcionar una vista. Esto se logra utilizando estructuras inmutables de tipo valor, que son predecible y de rápida creación y liberación al estar almacenadas en el stack.
SwiftUI utiliza una representación interna, en forma de árbol, para dibujar los elementos en pantalla. Este árbol de vista es eficiente y esta estrechamente relacionado con el estado de los datos que aparecen en pantalla. Los datos (como un Int, Bool, array o objeto) representan el estado, y las vistas en SwiftUI están vinculadas directamente con el estado; de echo, las vistas son una función directa del estado. La siguiente figura ilusta este principio de diseño declarativo basado en el estado:

En la figura, List es un nodo del árbol de vista que SwiftUI representa internamente como una estructura de árbol de nodos y que, en última instancia, se renderiza en un componente UI que además de su geometría también maneja gestos (eventos) y funciones de accesibilidad.
La Vista Raíz del árbol de vistas es generalmente el punto de entrada de la App (WindowGroup {…}, por ejemplo). Cada nodo del árbol deriva de una vista padre (contenedor), y puede contener un conjunto de valores de estado asociados, además de vistas hijas y funciones de decoración llamadas “Modificadores” en SwiftUI.
Un modificador es una función que acepta como parámetro una vista y devuelve una vista modificada. Los modificadores nos permiten decorar o cambiar la apariencia visual y comportamiento de una vista. Estos son parte del árbol de vistas de SwiftUI.
Observemos el siguiente código:
@main
struct ArchivadorApp: App {
@StateObject var model : CRUDModel = CRUDModel()
var body: some Scene {
WindowGroup {
HomeScreen()
.foregroundColor(.primary)
.environmentObject(model)
.onTapGesture{
print("Handle tap event")
}
}
}
}
Como podemos observar este código representa el punto de entrada de la app y su diseño es declarativo. Aqui vemos como se utiliza un struct que conforma el protocolo View; este protocolo tiene una única propiedad requerida: body, que es una propiedad calculada que devuelve un árbol de vistas. SwiftUI genera internamente una estructura como la siguiente:
WindowGroup
└── HomeScreen
|── Modifier: foregroundColor(.primary)
|── Modifier: environmentObject(model)
└── Modifier: onTapGesture {
print(“Handle tap event”)
}
En este ejemplo, HomeScreen representa el nodo raíz, este nodo no tiene vistas hijas directas, sin embargo tiene tres nodos que representan modificadores aplicados a la vista padre (foregroundColor, environmentObject y onTapGesture ). También podemos observar que se ha establecido una propiedad model con el atributo @StateObject esto le indica a SwiftUI que observe los cambios en esta propiedad y renderice las partes de la UI que dependen de dicha propiedad. Model, por su parte, representa el estado que se adjunta al árbol de vista a través de modificadores como “.environmentObject(model)”.
Ya veremos más adelante como funciona estas herramientas de administración del estado pero lo importante a saber aqui es que en cada rinderizado (invalidación) de la UI, SwiftUI mantiene el valor del estado para que la vista sea consistente. Esto nos lleva al tema principal: Veamos cuales son las herramientas que utiliza SwiftUI para administrar el estado de la UI.
@State
@State es una propiedad que administra valores locales mutables dentro de una vista. Está diseñada para valores simples que pertenecen exclusivamente a la vista donde se declaran.
Cuándo usarlo
•Cuando necesitas un estado privado que no se comparte con otras vistas.
•Para propiedades simples, como un Bool, Int, String o cualquier otro valor básico.
Ejemplo correcto
struct CounterView: View {
@State private var count = 0 // Propiedad local
var body: some View {
VStack {
Text("Contador: \(count)")
Button("Incrementar") {
count += 1
}
}
}
}
Ejemplo incorrecto:
struct CounterView: View {
var count = 0 // Sin @State, no se reactiva la vista
var body: some View {
VStack {
Text("Contador: \(count)")
Button("Incrementar") {
count += 1 // Error: No actualiza la vista
}
}
}
}
Por qué está mal: Sin @State, SwiftUI no observará los cambios en la propiedad count, por lo que la vista no se actualizará.
@StateObject
@StateObject administra el ciclo de vida de un objeto que conforma el protocolo ObservableObject. Es ideal para crear y mantener objetos que necesitan persistir durante la vida útil de una vista.
Cuándo usarlo
•Cuando una vista crea y administra una instancia de ObservableObject.
•Para objetos que contienen lógica de negocio o datos que deben ser persistentes.
Ejemplo correcto:
class CounterViewModel: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Contador: \(viewModel.count)")
Button("Incrementar") {
viewModel.count += 1
}
}
}
}
Ejemplo incorrecto:
class CounterViewModel: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel() // Error
var body: some View {
VStack {
Text("Contador: \(viewModel.count)")
Button("Incrementar") {
viewModel.count += 1
}
}
}
}
Por qué está mal: Al usar @ObservedObject para inicializar un objeto, se crea una nueva instancia cada vez que se renderiza la vista, lo que provoca pérdida de estado.
@ObservedObject
@ObservedObject observa un objeto que implementa el protocolo ObservableObject, pero no administra su ciclo de vida. Es útil para pasar datos entre vistas cuando el objeto es gestionado por otro componente.
Cuándo usarlo
•Cuando una vista observa un objeto, pero no es responsable de crearlo o mantenerlo.
•En vistas hijas que necesitan observar un objeto gestionado por una vista padre.
Ejemplo correcto
class CounterViewModel: ObservableObject {
@Published var count = 0
}
struct ParentView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
ChildView(viewModel: viewModel)
}
}
struct ChildView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
Text("Contador: \(viewModel.count)")
Button("Incrementar") {
viewModel.count += 1
}
}
}
}
Ejemplo incorrecto:
struct ChildView: View {
@ObservedObject var viewModel = CounterViewModel() // Error
var body: some View {
VStack {
Text("Contador: \(viewModel.count)")
Button("Incrementar") {
viewModel.count += 1
}
}
}
}
Por qué está mal: Como ya vimos en el ejemplo anterior, @ObservedObject no debe ser propietaria de la instancia del objeto que observa. Por lo tanto, solo debemos utilizarla para observar una instancia sin llegar a crearla. Si creamos una instancia con @ObservedObject se producirá una perdida del estado en cada renderizado.
@EnvironmentObject
@EnvironmentObject permite compartir un objeto entre múltiples vistas en una jerarquía, sin necesidad de pasarlo explícitamente a través de cada vista.
Cuándo usarlo
•Cuando un objeto debe estar disponible globalmente en una jerarquía de vistas.
•Para datos compartidos, como configuraciones o estados globales.
Ejemplo correcto
class AppSettings: ObservableObject {
@Published var isDarkMode = false
}
@main
struct MyApp: App {
@StateObject private var settings = AppSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings) // Se inyecta en el entorno
}
}
}
struct ContentView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Toggle("Modo oscuro", isOn: $settings.isDarkMode)
}
}
El modificador .environmentObject() inyecta la instancia en la jerarquía del árbol de vistas. De este modo la instancia queda disponible para cualquier vista por debajo del punto de inyección, en el árbol de vista. Observe la manera de tener acceso a la instancia en una vista hija con el uso del atributo @EnvironmentObject el cual actual como un “getter” que recupera la instancia del entorno.
Ejemplo incorrecto:
struct ContentView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Toggle("Modo oscuro", isOn: $settings.isDarkMode)
}
}
// Error: No se inyectó el objeto AppSettings en el entorno.
@Binding
@Binding se utiliza para compartir un estado mutable entre una vista padre y una hija, sin necesidad de pasar un objeto completo.
Cuándo usarlo
•Cuando una vista necesita modificar una propiedad específica de su vista padre.
Ejemplo correcto
struct ParentView: View {
@State private var isOn = false
var body: some View {
ChildView(isOn: $isOn)
}
}
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Activar", isOn: $isOn)
}
}
Ejemplo incorrecto:
struct ParentView: View {
@State private var isOn = false
var body: some View {
ChildView(isOn: isOn) // Error: Pasando el valor en lugar de un binding
}
}
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Activar", isOn: $isOn)
}
}
Por qué está mal: @Binding espera un enlace bidireccional a un estado, pero el ejemplo incorrecto pasa una copia del valor.
Cuadro Resumen
Herramienta | Uso correcto | Uso incorrecto |
@State | “Estados locales simples dentro de una vista.” | “Usarlo para estados complejos o compartidos.” |
@StateObject | “Crear y administrar un ObservableObject.” | “Reemplazarlo con @ObservedObject en vistas responsables del ciclo de vida.” |
@ObservedObject | “Observar objetos gestionados por otra vista.” | “Inicializar el objeto dentro de la vista.” |
@EnvironmentObject | “Compartir objetos globales entre vistas.” | “No inyectar el objeto en el entorno.” |
@Binding | “Compartir estados específicos entre vistas padre e hija.” | “Pasar valores en lugar de enlaces.” |
Usar estas herramientas correctamente garantiza que tu aplicación SwiftUI sea robusta, eficiente y fácil de mantener. ¡Comprender las diferencias y casos de uso es clave para aprovechar al máximo SwiftUI!
Si Desea conocer más puede ver este vídeo donde se explica los fundamentos de SwiftUI.
Si le ha sido útil este contenido puede dejarme un comentario. Si tiene alguna duda también puede ponerse en contacto y haré lo posible por resolverla en conjunto. Feliz codificación!😊