@State vs @Binding vs @ObservedObject vs @StateObject vs @EnvironmentObject — ¿cuál es la diferencia real?
Cinco property wrappers que dicen 'esta vista depende de un estado', pero difieren en quién es dueño del almacenamiento, cuándo se crea y hasta dónde se propaga un cambio — confundirlos es un tropiezo muy común en entrevistas de SwiftUI.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
@State vs @Binding vs @ObservedObject vs @StateObject vs @EnvironmentObject — ¿cuál es la diferencia real?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Los cinco se ven casi idénticos en el lugar donde se usan — @Algo var x — y justamente por eso funcionan tan bien como filtro en una entrevista: la diferencia entre ellos no pasa por qué tipo de dato guarda cada uno, pasa por dos preguntas ortogonales. ¿Quién es dueño del almacenamiento, y cuándo se crea? Y ¿hasta dónde se propaga una escritura? Respondé esas dos preguntas para cada wrapper y toda la tabla sale sola.
@State es para un tipo valor que es dueño esta vista. La sintaxis da la impresión de que el valor vive adentro del struct de la vista, pero no es así — SwiftUI le asigna almacenamiento persistente por fuera del struct, atado a la identidad de la vista, justamente porque el struct de la vista se descarta y se vuelve a crear en cada evaluación del body de su padre. El almacenamiento de @State es lo que sobrevive a ese descarte constante. Casi siempre debería ser private, porque nada de afuera de esta vista tiene que tocarlo directamente — si un hijo necesita leerlo o escribirlo, le pasás un Binding, no el valor crudo.
@Binding no es almacenamiento en absoluto. Es una referencia get/set hacia un estado que es dueño otra cosa — normalmente el @State de un padre, o la propiedad @Published de un objeto, expuesta con el prefijo $. Un hijo que declara @Binding var isOn: Bool lo puede leer y escribir exactamente como si fuera un @State local, pero cada escritura viaja de vuelta hasta la verdadera fuente de verdad, y esa escritura es la que realmente dispara el re-render — el binding en sí no guarda nada.
@ObservedObject es para tipos referencia que conforman ObservableObject, y el dato crítico es que la vista que lo usa no es dueña del ciclo de vida del objeto — solo observa una instancia que ya existe, típicamente pasada desde un padre. Acá vive el bug real más común del SwiftUI en producción: declarar @ObservedObject var vm = ViewModel() con el inicializador inline hace que esa expresión corra en cada init del struct que la contiene — lo que significa que cada vez que el padre se re-renderiza y vuelve a crear el valor de esta vista hija, obtenés en silencio un ViewModel completamente nuevo, y todo lo que ese objeto venía guardando desaparece.
@StateObject existe específicamente para tapar ese agujero. El mismo requisito de ObservableObject, pero SwiftUI garantiza que la expresión del valor inicial corre exactamente una vez por identidad de vista, y la instancia resultante sobrevive sin importar cuántas veces se re-renderice el padre. La regla práctica que resuelve casi cualquier confusión: si esta vista crea el objeto, es @StateObject; si solo recibe un objeto que ya creó otra cosa, es @ObservedObject.
@EnvironmentObject es un ObservableObject inyectado en el entorno por algún ancestro vía .environmentObject(_:), disponible implícitamente para cualquier descendiente que declare @EnvironmentObject var x: T — sin necesidad de pasarlo a mano por cada inicializador intermedio. El costo es real: es cómodo para algo genuinamente global a la app (una sesión de autenticación, un tema, un carrito), pero la dependencia se vuelve invisible en el lugar donde se usa, y si algún ancestro se olvidó del .environmentObject(_:), eso es un crash en runtime, no un error de compilación.
struct ProfileScreen: View {@State private var isEditing = false// Es dueño de este objeto: se crea acá, tiene que sobrevivir a los re-renders de esta vista.@StateObject private var viewModel = ProfileViewModel()// Inyectado por algún ancestro vía .environmentObject(_:) más arriba.@EnvironmentObject var session: SessionManagervar displayName: String {session.currentUser?.name ?? "Invitado"}var body: some View {VStack {Text("Sesión iniciada como \(displayName)")// $isEditing convierte el @State en un Binding<Bool> para el hijo.EditToggleRow(isEditing: $isEditing)// Se pasa hacia abajo, no se crea acá — el hijo solo lo observa.ProfileDetailView(viewModel: viewModel)}}}struct EditToggleRow: View {@Binding var isEditing: Boolvar body: some View {Toggle("Modo edición", isOn: $isEditing)}}struct ProfileDetailView: View {// NO es dueño del ciclo de vida del objeto — solo observa uno que le pasaron.@ObservedObject var viewModel: ProfileViewModelvar body: some View {TextField("Nombre visible", text: $viewModel.displayName)}}final class ProfileViewModel: ObservableObject {@Published var displayName: String = ""}final class SessionManager: ObservableObject {@Published var currentUser: User?}struct User {let name: String}
Vale la pena cerrar mencionando hacia dónde va todo esto: el macro @Observable de iOS 17 junto con @Bindable colapsa parte de esta tabla. Las clases simples marcadas @Observable ya no necesitan @Published para nada, una vista que es dueña de una usa @State en vez de @StateObject (la regla de propiedad queda idéntica, solo cambia el nombre del wrapper para tipos referencia), y una que solo la recibe usa una propiedad común en vez de @ObservedObject. La pregunta de los cinco wrappers de arriba se sigue haciendo porque la mayoría de los códigos base en producción todavía están a años de terminar esa migración — pero nombrar hacia dónde va la cosa es exactamente el tipo de detalle que separa "esto lo memoricé" de "esto lo uso".
🧠 Check your understanding
0/3 · 0/3 answered1. Una vista declara @ObservedObject var vm = ProfileViewModel() directamente, sin recibir ningún valor de un padre. ¿Cuál es el riesgo?
2. ¿Cuál de estos no es almacenamiento en absoluto, sino una referencia de ida y vuelta hacia un estado que es dueño otra cosa?
3. ¿Por qué un equipo podría preferir @EnvironmentObject para un SessionManager compartido entre una docena de pantallas sin relación entre sí?