Ir al contenido

Plataformas

visionOS

Angular en el Vision Pro — el mismo núcleo, el mismo host que iOS, y una ventana que no es una pantalla.

Angular en el Vision Pro: mismo núcleo, mismo layout, mismo bundle y el mismo host. visionOS trae UIKit con jerarquía de UIView y marcos absolutos, igual que iOS y que tvOS, así que an-ios compila para el visor sin reescribir nada.

Ventana de terminal
cargo an visionos # examples/hello-vision en el simulador
./scripts/check-visionos.sh

Lo que cambia de verdad no es el catálogo de controles —está casi entero—, es de dónde sale la ventana y qué hay detrás de ella.

Es la tercera rama del mismo enum Family de crates/an-cli/src/ios.rs, y hereda todo lo que ese módulo ya sabía: el Info.plist que puede aportar un proyecto de fuera, la comprobación de que ese plist dice lo mismo que el proyecto, y build_dir().

visionOS
Target de Rust aarch64-apple-visionos-sim (nivel 3)
Triple de swiftc arm64-apple-xros1.0-simulator
SDK de xcrun xrsimulator
Runtime en simctl aparece como xrOS
Info.plist shells/visionos/Resources
Sufijo del .app Vision / .vision
std de Rust se construye con -Z build-std

El nombre comercial cambió y el del compilador no: en el triple de swiftc, en la variable de despliegue (XROS_DEPLOYMENT_TARGET) y en el nombre del runtime de simctl, visionOS sigue siendo xrOS. Buscar «visionOS» en la lista de simctl no encuentra nada.

Esta es la diferencia que toca al modelo de marcos absolutos, y conviene mirarla de frente antes de dar por bueno que «se ve igual que iOS».

No hay UIScreen. Está marcado API_UNAVAILABLE(visionos), así que UIScreen.main.bounds no compila siquiera. Una app no ocupa una pantalla: ocupa una ventana que el usuario cuelga en la habitación, mueve y redimensiona tirando de la esquina cuando quiere. De esa ventana solo se sabe a través de su UIWindowScene, y el sistema solo crea uno si el manifiesto declara quién lo atiende:

Info.plist shells/ios/Sources/SceneDelegate.swift
UIApplicationSceneManifest ──────▶ @objc(SceneDelegate)
UISceneDelegateClassName scene(_:willConnectTo:options:)
= "SceneDelegate" └─▶ UIWindow(windowScene:)

Ese nombre es el de Objective-C, el que le pone @objc a la clase de Swift. Si los dos dejaran de coincidir, la escena se conectaría, nadie crearía la ventana y la app arrancaría en negro sin un solo error: por eso check-visionos.sh compara las dos cadenas.

Al modelo de marcos absolutos no le pasa nada. El core nunca preguntó por una pantalla: recibe un viewport y coloca. Lo que cambia es que ese viewport ya no es una constante durante toda la vida de la app. El shell pide un tamaño al abrir —requestGeometryUpdate(.Vision(size:)), que es una preferencia: el sistema puede dársela o no— y el tamaño de verdad llega por viewDidLayoutSubviews, igual que en las otras dos familias. Lo que sí cambia es lo que puede escribir una plantilla: nada en puntos fijos. Un ancho de 390 es una suposición sobre una pantalla que aquí no existe; examples/hello-vision está escrito con porcentajes y flexGrow, y eso es lo que comprueba el script.

deviceInfo.scale sale 0. No hay escala de pantalla que preguntar: la app se dibuja para dos ojos y a la distancia a la que el usuario haya puesto la ventana. Se devuelve 0 y se dice aquí, en vez de inventarse un 2.0 que alguien acabaría usando para calcular píxeles.

La ventana de visionOS ya trae fondo: el cristal que dibuja el sistema, con su desenfoque y su sombra sobre la habitación de verdad. El shell deja la raíz transparente (view.backgroundColor = .clear) en vez del negro que pone en las otras dos familias.

Un [backgroundColor] en el contenedor de arriba lo tapa entero, y lo que queda es una losa opaca flotando en el salón. No es un error —la app se ve— y por eso está comprobado: check-visionos.sh mira que el contenedor raíz de hello-vision siga sin pintar nada.

visionOS sí tiene toques: el pellizco con la mano cuenta como un toque indirecto y llega por los mismos reconocedores que en iOS, así que aquí no hace falta ningún motor de foco como el de la tele. Lo que cambia es cómo sabe el usuario qué va a pulsar: apunta con la mirada, y sin realce no hay forma de ver dónde está apuntando.

Ese realce lo dibuja el sistema, fuera del proceso de la app, pero solo si la vista lo pide con hoverStyle. Una UIView con un reconocedor de toque no lo pide: el valor de fábrica es nil. Por eso el host le pone automaticStyle a toda vista que se hace pulsable (crates/an-ios/src/hover.rs). automaticStyle es el realce del sistema con la forma que UIKit deduce de la vista: no se dibuja nada a mano, y si visionOS cambia su aspecto en una versión, esto cambia con él.

Y lo que no hay:

Evento Qué pasa en visionOS
(back) No hay ningún gesto del sistema para volver: UIScreenEdgePanGestureRecognizer no está en el SDK y la ventana no tiene bordes que arrastrar. Se cierra por su barra, y dentro de la app el camino de vuelta tiene que ser un botón de la plantilla. Se dice en el log.

Esto es importante y va aparte, porque no todo lo de arriba tiene el mismo respaldo.

Visto corriendo en el simulador de visionOS 26.5: la app arranca, la ventana nace de la escena, el árbol se monta con los marcos que calculó taffy, el texto se mide con el UIFont de verdad, el cristal del sistema se ve por detrás y por entre las tarjetas, y una pulsación llegó hasta la señal del componente y cambió el rótulo.

No verificado: no he conseguido provocar esa pulsación de forma repetible desde fuera. xcrun simctl no tiene ningún verbo para el puntero, y un clic sintético sobre la ventana del simulador —con cliclick, con las coordenadas de la ventana bien calculadas— no llega a la app: el simulador de visionOS usa el ratón para mirar alrededor y para pellizcar, y esa entrada no se deja conducir desde la línea de órdenes como sí se deja la del mando de la tele. Lo que hay, por tanto, es una captura con el resultado de una pulsación y ninguna forma de repetirla en un script. Mientras eso siga así, aquí no hay equivalente de scripts/tv-remote.sh, y check-visionos.sh comprueba la pulsación por el camino de siempre: el renderer sin pantalla.

Tampoco verificado: el realce de la mirada. El host le pone hoverStyle a las vistas pulsables, pero sin poder mover la mirada desde fuera no he podido ver el realce puesto en una captura.

  • deviceInfo.platform sigue diciendo "ios" en las tres familias. Una app que quiera adaptarse al visor —o a la tele— no tiene hoy forma de saber dónde está. El arreglo no está en an-ios: el tipo NativeDeviceInfo.platform de packages/primitives declara los valores que puede tomar, y hay que abrirlo ahí primero.
  • Icono. El Info.plist no lleva CFBundleIcons: el icono de visionOS es en capas y vive en un catálogo de assets compilado con actool. La app se instala y se lanza; en la parrilla sale sin icono.
  • Nada volumétrico. Esto es una ventana plana en un espacio 3D, que es lo que visionOS llama una window. Ni volume ni espacio inmersivo: las dos cosas son SwiftUI y RealityKit, y no hay UIView que montar en ellas, así que no serían este host sino otro, como pasó con el reloj.
  • Dispositivo de verdad. Todo esto está visto en el simulador. Un Vision Pro físico pediría firma, y la mirada de verdad no es un ratón.