@StateObject es un Property Wrapper en SwiftUI para crear y poseer una instancia de ObservableObject directamente en una vista. SwiftUI garantiza que el objeto se inicialice una vez por ciclo de vida de la vista y no se vuelva a crear en renderizaciones posteriores. Según la Documentación para Desarrolladores de Apple (2025), se recomienda @StateObject para vistas raíz que crean una fuente de datos. @StateObject es la opción correcta para poseer un ObservableObject en la jerarquía de SwiftUI.
Puntos clave
@StateObject es un Property Wrapper introducido en SwiftUI 2.0 (iOS 14) que combina las capacidades de @ObservedObject y @State. Al igual que @ObservedObject, se suscribe a los cambios de ObservableObject. Como @State, garantiza que los datos sobrevivan a inicializaciones repetidas de la estructura de la vista. @StateObject crea el objeto una vez cuando la vista aparece por primera vez y lo almacena en el heap de SwiftUI.
Antes de @StateObject, los desarrolladores usaban @ObservedObject para todos los ObservableObjects, incluidos los creados en vistas. Esto provocaba pérdidas frecuentes de datos cuando la vista padre se actualizaba, lo que hacía que la estructura de la vista se recreara y se llevara consigo la instancia de @ObservedObject. @StateObject resolvió esto añadiendo una garantía de estabilidad.
La regla principal: @StateObject se usa en la vista que crea el objeto en el inicializador por defecto (let model = ViewModel()). Las vistas hijas que reciben este objeto usan @ObservedObject. Esta separación garantiza una única fuente de verdad en toda la jerarquía.
SwiftUI gestiona el ciclo de vida de @StateObject a través de un gestor de almacenamiento similar a @State. Cuando la vista aparece por primera vez, SwiftUI asigna memoria para el objeto y lo almacena en un área persistente. En renderizaciones posteriores (llamadas a body), el objeto no se recrea—se usa la instancia existente. El objeto vive mientras la vista esté en la jerarquía.
Cuando la vista se elimina de la jerarquía, SwiftUI destruye el @StateObject, llamando a deinit. Cuando la vista se vuelve a añadir a la jerarquía, se crea una nueva instancia. Esto es importante al diseñar: si necesita conservar datos entre eliminaciones de vistas, use una capa de servicio (singleton o DI) o @AppStorage para la persistencia.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
En el ejemplo, TimerViewModel se crea mediante @StateObject y vive mientras TimerView esté en pantalla. El temporizador se inicia en onAppear y se detiene en deinit. Si se usara @ObservedObject, cada renderización de TimerView crearía un nuevo TimerViewModel con segundos = 0, y el temporizador nunca funcionaría correctamente. @StateObject garantiza que el viewModel sea único y estable.
La elección entre @StateObject y @ObservedObject depende de quién posee el objeto. Si la vista crea el objeto—@StateObject. Si la vista recibe un objeto ya creado—@ObservedObject. Esta regla es tan importante que Xcode muestra una advertencia al usar @StateObject en una vista hija que recibe el objeto a través de un inicializador.
| Situación | Wrapper recomendado |
|---|---|
| La vista crea el modelo mediante ViewModel() | @StateObject |
| La vista recibe el modelo del padre | @ObservedObject |
| El modelo se usa en una sola vista | @StateObject |
| El modelo se pasa a través de Environment | @EnvironmentObject |
| El modelo se necesita para previsualizaciones | @ObservedObject + mock |
En la práctica, al inicio de un proyecto, a menudo se usa @StateObject en la vista raíz y @ObservedObject en todas las vistas hijas. A medida que la aplicación crece, algunas instancias de @StateObject pueden reemplazarse por @EnvironmentObject para simplificar la jerarquía. Sin embargo, @StateObject sigue siendo la mejor opción para pantallas modulares con lógica propia.
El primer patrón—MVVM con @StateObject. El ViewModel como ObservableObject se crea en la vista mediante @StateObject. El ViewModel contiene propiedades @Published y lógica de negocio. La vista se suscribe a los cambios y actualiza la interfaz. Este enfoque proporciona un aislamiento comprobable: el ViewModel se puede probar sin interfaz de usuario creando una instancia directamente.
El segundo patrón—@StateObject con dependencias. Si el ViewModel requiere servicios, use la inicialización con parámetros. Por ejemplo, @StateObject var viewModel = UserViewModel(api: APIClient.shared). Sin embargo, tenga cuidado: los parámetros se calculan en cada renderización de body, pero el objeto se crea solo una vez. SwiftUI ignora las inicializaciones posteriores de @StateObject.
El tercer patrón—@StateObject anidados. En SwiftUI, puede tener varios @StateObject en una vista, pero esto rara vez se justifica. Normalmente un @StateObject maneja todo el conjunto de datos de la vista. Si la lógica se vuelve demasiado compleja, divídala en una composición de servicios @ObservedObject dentro de un @StateObject.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
En el ejemplo, AppView crea dos @StateObject: NavigationRouter para gestionar la navegación y AuthViewModel para la autenticación. Ambos objetos se inyectan en el Environment a través de environmentObject. Cualquier vista hija puede acceder a ellos mediante @EnvironmentObject sin pasar por la cadena de inicializadores.
@StateObject admite la inicialización con cualquier parámetro, pero con una salvedad importante: el inicializador se llama solo una vez. En renderizaciones posteriores de body, los nuevos valores de los parámetros se ignoran. Esto significa que si pasa @State var id: Int = 5 a @StateObject var vm = ViewModel(id: id), cuando id cambie, el ViewModel no recibirá el nuevo valor.
Para resolver este problema, use onReceive u onAppear para la sincronización. Suscríbase a los cambios de parámetros dentro del ViewModel a través de Combine o pase parámetros mediante el método .onChange(of:) a nivel de vista. Una alternativa es usar @ObservedObject en lugar de @StateObject si el objeto debe responder dinámicamente a cambios externos.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
El enfoque correcto: DetailView recibe itemId como una propiedad let (pasada a través del inicializador de la estructura), y @StateObject crea DetailViewModel sin parámetros. En onAppear, se llama al método load(id:) para cargar datos para el ID recibido. Esto garantiza que el ViewModel sea creado por el mecanismo @StateObject, pero los datos se cargan en cada aparición de la vista con el ID actual.
El principal error—usar @StateObject en vistas hijas que reciben el objeto de un padre. Si ParentView crea @StateObject model, y ChildView declara @StateObject var model: ModelType (con un parámetro por defecto), ChildView creará su propia instancia independiente. Los objetos padre e hijo no estarán conectados, y los cambios en uno no se reflejarán en el otro.
El segundo error—colocar @StateObject en List o ForEach. Cada elemento de la lista crea su propio @StateObject, lo que genera múltiples instancias independientes. Para listas, el enfoque correcto es pasar un único ObservableObject a todos los elementos mediante @ObservedObject o usar estructuras Identifiable con @State dentro de List.
El tercer problema—falta de limpieza en deinit. @StateObject vive durante todo el ciclo de vida de la vista. Si el objeto crea temporizadores, suscripciones Combine o solicitudes de red, deinit debe cancelarlos. De lo contrario, las pérdidas de memoria y el trabajo en segundo plano continuo después del cierre de la pantalla son inevitables. Use siempre un almacén Cancellable de Combine o invalide los temporizadores en deinit.
Preguntas frecuentes
@StateObject se agregó en SwiftUI 2.0 en la WWDC 2020 junto con iOS 14, macOS 11, watchOS 7 y tvOS 14. Antes de eso, @ObservedObject era la única forma de trabajar con ObservableObject, lo que a menudo provocaba errores de pérdida de datos.
No, @StateObject no admite tipos Optional. El objeto debe inicializarse en la declaración. Si necesita un objeto opcional, use @ObservedObject o @EnvironmentObject con un tipo opcional.
Agregue print(#function) al inicializador y deinit del ObservableObject. Si init no se llama en renderizaciones posteriores—@StateObject funciona correctamente. Si init se llama cada vez—reemplace @ObservedObject por @StateObject.
Sí, @StateObject funciona en vistas de SwiftUI incrustadas en UIKit mediante UIHostingController. El ciclo de vida del objeto está vinculado a la vista de SwiftUI, no al UIViewController. Si la vista de SwiftUI se reemplaza, el @StateObject se destruye.
Varios @StateObjects pequeños con responsabilidades separadas. Esto mejora la comprobabilidad, la reutilización y el rendimiento—cuando un objeto cambia, solo se vuelven a dibujar las partes suscritas de la interfaz, no toda la vista.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también