[ES] Migrando mi SlideMenu a @Observable: dos cosas que no esperaba al medir
En el post anterior monté un ViewModel para gestionar el SlideMenu de un TabView, y acabé escribiendo un Binding manual con get y set porque el atajo $viewModel.option no me funcionaba. Con @Observable disponible desde iOS 17, me pregunté si la migración cambiaba algo. La hice y la medí con UI tests en simuladores, y salieron dos resultados que no esperaba.
La migración
El ViewModel pierde Combine y @Published, y gana @Observable. originalOption lleva @ObservationIgnored, porque ninguna vista lo lee:
En la vista, @StateObject pasa a @State:
El didSet que devuelve la pestaña anterior al pulsar “Menu” sigue funcionando bajo @Observable. El diff completo son dos ficheros.
Sorpresa 1: @Observable no arregla el atajo $
Esperaba que $viewModel.option funcionara tras migrar. Lo probé con un UI test que pulsa “Menu” y comprueba que sigue seleccionada la pestaña anterior y que se ve su contenido:
| iOS (simulador) | v1 (ObservableObject) | v2 (@Observable) |
|---|---|---|
| 17.2 | Falla con $ | Falla con $ |
| 18.1 a 18.6, 26.2, 27.0 | Pasa | Pasa |
Lo que separa los resultados es el runtime, no la migración. En 17.2, con $, el menú se abre pero se queda seleccionada la pestaña “Menu” y se ve el contenido verde. El Binding manual pasa en todos los runtimes, en v1 y en v2. Si tu target incluye iOS 17.x, mi recomendación es mantenerlo. Solo probé el 17.2, así que la frontera está entre 17.2 y 18.1, y no sé por qué falla en 17.2.
Sorpresa 2: el comportamiento de @State depende de Xcode
Quería comprobar si @State crea el ViewModel una vez o en cada reevaluación del padre, como hace @StateObject. Monté un RootView que construye ContentView() dentro de su body, con un contador que fuerza reevaluaciones, y conté cuántas veces se ejecuta el init del ViewModel al arrancar y tras 3 pulsaciones:
| Compilado con | @StateObject | @State |
|---|---|---|
| Xcode 26.2 (Swift 6.2.3) | 1 → 1 | 1 → 4 |
| Xcode 27.0 (Swift 6.4) | 1 → 1 | 1 → 1 |
Lo determina el compilador, no el sistema donde corre la app: un binario de Xcode 27 da 1 → 1 en iOS 17.2 y uno de Xcode 26.2 da 1 → 4 en iOS 27. Con Xcode 26.2 reproduje el comportamiento clásico: @State reconstruye el ViewModel, sea @Observable o ObservableObject. Con Xcode 27 no lo veo en ninguno de los casos.
Apple lo explica en la sesión What’s new in SwiftUI de WWDC26: State pasa a ser un macro, las clases guardadas en él se inicializan una sola vez, y según Apple esto llega también a las versiones desde iOS 17. Lo que yo he comprobado es el resultado, no el mecanismo interno. Si compilas con Xcode 26.x, @StateObject sigue siendo la opción segura para no recrear el ViewModel.
Un aviso de la misma sesión que no he reproducido: con el macro, si un @State tiene valor por defecto y además lo asignas en el init, Xcode 27 da un error de uso antes de inicializar. Apple recomienda quitar el valor por defecto.
Cómo lo medí y qué no cubre
Todo sale de UI tests y de un script, con las tablas completas en RESULTS.md y el script en scripts/run_matrix.sh. Cada celda del Binding se ejecutó 3 veces, y cada celda de inits una vez. Las pruebas del Binding son solo con Xcode 27, en simuladores de iPhone, sin iPad ni dispositivo físico ni otros 17.x. Una ejecución del test falló una vez por una pulsación no registrada y pasó en las dos repeticiones siguientes.
El proyecto está en TabViewWithSideMenuObservable. Los tags v1-observableobject y v2-observable permiten ver el antes y el después.
Comments are closed here.