Plataformas
watchOS
Angular en el Apple Watch — el único host que no es una jerarquía de vistas, la corona digital, y los ocho primitivos que el SDK no tiene.
El mismo core, el mismo layout, el mismo bundle. Lo que cambia es quién pinta.
an watchos # examples/hello-watch en el simuladoran watchos examples/watch-controls # todo lo que el reloj sabe dibujaran dev --watchos examples/watch-controls./scripts/check-watchos.shPor qué esto no es el host de iOS con un cfg
Sección titulada «Por qué esto no es el host de iOS con un cfg»watchOS no tiene jerarquía de UIView. La interfaz es SwiftUI y no hay vuelta
que darle: no hay contenedor al que añadir subvistas ni frame que mover.
Eso choca de frente con el modelo de montaje de este proyecto, que es imperativo —crear una vista, ponerla aquí, cambiarle el color— mientras que SwiftUI solo acepta estado: describes lo que hay y él decide qué redibujar.
La salida es reflejar el árbol que mantiene Rust en un modelo que SwiftUI
observa, y convertir cada MountOp en una mutación de ese modelo.
Rust Swift ──────────────────────────────── ───────────────────────────── QuickJS ─▶ ShadowTree ─▶ taffy Timer a 30 Hz │ commit │ an_watch_runtime_frame ▼ ▼ Frame { MountOp[] } ¿cambió la revisión? │ │ sí ▼ ▼ WatchHost (modelo) ──JSON──▶ AnTree (@Observable) │ ▼ ZStack + .positionWatchHost implementa HostRenderer pero no posee ninguna vista: un MountOp
aterriza en un HashMap<NodeId, WatchNode> y cada mutación incrementa un
contador de revisión. Una vez por fotograma, snapshot() serializa el árbol
entero a JSON — una foto, nunca un diff. Una pantalla de reloj son diez o
quince nodos, así que Codable lo descodifica sin parser escrito a mano y un
cruce de FFI sustituye a los cientos que costaría una llamada por MountOp.
AnTree lee primero la revisión y vuelve enseguida cuando no ha cambiado, así
que un fotograma quieto no descodifica nada y SwiftUI no recompone.
El hilo de JS corre con una pila de 8 MB y un presupuesto de 25 ms por
fotograma: 33 ms a 30 Hz, con 8 ms de sobra para que SwiftUI recomponga.
CADisplayLink no existe en watchOS, y por eso el bucle es un Timer.
El layout sigue siendo el de taffy. No hay ni un VStack, ni un HStack,
ni un .padding en todo el shell. Dos motores de layout decidiendo lo mismo
significa que gana el que corra el último, así que cada nodo se coloca con
.frame(width:height:) más .position —.position, no .offset, que
desplazaría la vista desde donde SwiftUI la hubiera centrado— dentro de un
ZStack(alignment: .topLeading), que es el sistema de coordenadas que entrega
taffy. check-watchos.sh lo hace cumplir: un VStack o un .padding que
aparezca en los ficheros que dibujan el árbol suspende la comprobación.
Los colores se resuelven a RGBA 0..1 en Rust. Swift no parsea nunca #rrggbb.
La cadena de herramientas, que es la parte que puede no estar
Sección titulada «La cadena de herramientas, que es la parte que puede no estar»aarch64-apple-watchos-sim es un target de nivel 3: rustup lo lista pero no
trae std precompilado. Hay que compilarlo en el momento, y eso necesita
nightly:
rustup toolchain install nightly --component rust-srcUn solo comando, y es el que corre CI. Un nightly sin rust-src se salta esto
igual que no tener nightly, así que el componente no es un segundo paso
opcional.
Por eso an watchos llama a cargo +nightly en lugar de al toolchain de
rust-toolchain.toml, que fija estable con los dos targets de iOS y los dos de
Android — todos los targets a los que compila cruzado la suite y cuyo std
rustup sabe instalar. Los tres de nivel 3 de Apple están deliberadamente fuera:
rustup no puede instalar lo que no distribuye. check-watchos.sh se salta el
paso de compilación cruzada —diciéndolo— cuando no encuentra un nightly con
rust-src: que a alguien le falte un toolchain no debería tumbar el resto de
las comprobaciones.
No hay .xcodeproj. an watchos llama a xcrun swiftc directamente con
-sdk watchsimulator, -target arm64-apple-watchos11.0-simulator y
-parse-as-library — sin esa última bandera swiftc trata el @main de SwiftUI
como un script de nivel superior y no se usa nunca, en silencio. Las fuentes son
shells/watchos/Sources más un fichero de shells/shared.
QuickJS compila y corre en el reloj sin tocar nada. Era el gran riesgo del port y no se materializó.
Los diecisiete que dibuja
Sección titulada «Los diecisiete que dibuja»Todos son el control del sistema, no un dibujo que se le parece: traen las hápticas, el resaltado y el comportamiento de la corona que watchOS les da.
| Primitivo | Qué es en el reloj |
|---|---|
an-view |
ZStack(alignment: .topLeading) — fondo, esquinas (un radio o cuatro), borde, opacidad, transformación y los gestos que pida la plantilla. |
an-text |
Text. Fuente, peso, cursiva, familia, letterSpacing, subrayado y tachado, alineación y numberOfLines. Medido con la UIFont de verdad. |
an-button |
Button con .buttonStyle(.plain): el resaltado y las hápticas son del sistema, el fondo es de la app. |
an-scroll-view |
ScrollView(.vertical). La corona lo desplaza porque SwiftUI lo desplaza — eso no es algo que merezca la pena imitar. |
an-image |
Del bundle o de una URL http(s), a través de una caché respaldada por un actor; informa de su tamaño natural con (load), que es lo que necesita el layout para colocarla. |
an-icon |
Image(systemName:). El nombre se traduce a un SF Symbol en Rust, en an_core::icons. |
an-switch |
Toggle().labelsHidden(). |
an-slider |
Slider. En el reloj viene con el menos y el más a los lados, que es su forma allí. |
an-stepper |
Stepper, los dos botones del sistema. |
an-progress-bar |
ProgressView(value:). |
an-activity-indicator |
ProgressView() indeterminado; con [animating] a falso pasa a ser Color.clear, como se comporta hidesWhenStopped. |
an-text-input |
TextField / SecureField. Tocarlo abre la pantalla de entrada propia del reloj —dictado, escritura a mano o teclado— y devuelve el texto. |
an-select |
Picker().labelsHidden(): la rueda que gira la corona. En un reloj no hay desplegable. |
an-date-picker |
DatePicker, el selector de esfera del reloj. |
an-stack-view |
ZStack mostrando el último hijo, con transiciones: pop entra por delante y sale por detrás, none es la identidad, y por defecto es la inversa. .easeOut en 0,25 s, o lo que diga [animate]. |
an-alert |
.alert, con sus botones y su (select). Sin botones se le pone un OK. |
an-modal |
.sheet, o .fullScreenCover cuando [presentation] es fullScreen. |
Dos cosas que una plantilla tiene que saber:
an-text-inputnecesita[style.height]. taffy lo mide como mide un<Text>—una línea— porque en iOS unUITextFieldsin borde es exactamente eso. En el reloj el campo siempre se dibuja dentro de su propio contenedor redondeado, de 40 puntos de alto sea cual sea el frame que se le dé, así que sin una altura explícita se come la fila de abajo. El shell lo registra una vez cuando el frame llega más bajo que eso..textFieldStyle(.plain)no quita el contenedor: se probó en watchOS 26 y no cambia nada.an-alertyan-modalsalen del árbol. En SwiftUI no son vistas que coloques, son modificadores de la raíz, así que Rust los envía aparte, enoverlays, y el shell los cuelga deRootView. Unan-modalsí sigue ocupando sitio en el layout —eso lo decide el core, exactamente igual que en iOS— y en una pantalla de 248 puntos de alto eso es media pantalla: dale[style.position]="'absolute'"con el tamaño de la pantalla, que es también el frame en el que se maqueta su contenido.
Los nueve que no
Sección titulada «Los nueve que no»Con el motivo, que casi siempre es del SDK y no una opinión. Cuando el árbol
pide uno de estos, el nodo no se crea, se deja el hueco que midió el layout, y
el host lo dice una vez — con el motivo, no con una caja etiquetada. El shell
dibuja Color.clear al tamaño maquetado a propósito.
| Primitivo | Por qué no |
|---|---|
an-tab-bar |
Una barra de pestañas no cabe en 205 puntos de ancho. Lo que hace un reloj es deslizar entre secciones a pantalla completa — un contenedor, no una barra con un frame, así que es otro primitivo. |
an-navigation-bar |
La franja de arriba de un reloj ya es del sistema: la hora y el título de la app. Una barra nuestra pintaría debajo o encima. |
an-segmented-control |
SegmentedPickerStyle es @available(watchOS, unavailable) en SwiftUI. Lo que usa el reloj en su lugar es an-select. |
an-search-bar |
La búsqueda en un reloj es una pantalla del sistema, no un campo con una lupa. .searchable existe, pero es un modificador de navegación, no una vista con marco. |
an-textarea |
TextEditor es @available(watchOS, unavailable). El texto largo se dicta o se escribe a mano, y an-text-input ya te da eso. |
an-web-view |
WebKit no está en el SDK de watchOS. |
an-map-view |
El Map de SwiftUI sí existe en watchOS, pero no acepta ni centro ni zoom de la app: mostraría un lugar que la plantilla no eligió. |
an-video-view |
AVKit en watchOS no trae ni AVPlayerViewController ni VideoPlayer. Sus cabeceras declaran tipos y ninguna vista de reproducción. |
an-custom |
La vista que trae un plugin es una vista nativa que escribió otro, y este host no tiene jerarquía donde meterla: refleja el árbol en un modelo que SwiftUI redibuja. El protocolo de plugins del reloj no tiene attach por el mismo motivo. |
check-watchos.sh comprueba que la lista de soportados y la de no soportados
cubren juntas todo el vocabulario, y que ningún primitivo aparece en las dos ni
en ninguna.
Mover cosas
Sección titulada «Mover cosas»[animate], [translateX], [translateY], [scale], [scaleX], [scaleY] y
[rotate] son props de base —las tiene cualquier primitivo— y en el reloj cada
una es un modificador de SwiftUI.
| Prop | En el reloj |
|---|---|
[translateX], [translateY] |
.offset(x:y:). |
[scale], [scaleX], [scaleY] |
.scaleEffect(x:y:). scale pone los dos ejes y las dos con nombre pisan el eje que nombran; las tres se pliegan en dos en Rust, así que el shell lee un número por eje. |
[rotate] |
.rotationEffect(.radians:). Radianes, la unidad que reporta (rotation). |
[animate], [animateDelay], [animateEasing] |
.animation(_:value:), con ease-out por defecto y .delay cuando lo hay. Milisegundos en la plantilla, segundos en SwiftUI. |
SwiftUI encaja mejor que UIKit en esto, no peor: UIView.animate necesita
que el estado de partida esté puesto antes de entrar en el bloque, y aquí el
valor del que viene la vista sigue en el árbol de vistas. Lo que faltaba nunca
fue el dibujo: era que nadie llevaba los números al otro lado.
Tres cosas se deciden en Rust o en la colocación, no al dibujar:
- Una transformación no participa en el layout. taffy ya colocó el nodo y nada de esto lo mueve de ahí: una vista escalada sigue ocupando su caja sin escalar. Es el contrato en todos los hosts, y es lo que hace una transformación barata como para seguir un dedo con ella.
- El orden es escalar, luego girar y luego trasladar, que es el orden en que
an-iosmultiplica suCGAffineTransform. Al revés, el giro arrastra la traslación y arrastrar algo inclinado se va en diagonal en vez de seguir al dedo. - Los modificadores van antes de
.position..positiondevuelve una vista del tamaño del contenedor entero con el contenido colocado dentro, así que un.scaleEffectdespués escalaría respecto a ese centro y el nodo viajaría por la pantalla en lugar de crecer donde está.
Lo que cubre [animate] es el marco, la opacidad y la transformación — las tres
cosas que an-ios mete dentro de un bloque de animación. Que cambie un color o
una etiqueta es un corte en todos los hosts. La identidad no viaja nunca: un
desplazamiento de cero, una escala de uno y ningún giro son una clave ausente,
que es también lo que produce una plantilla que anima una transformación de
vuelta a nada.
El modificador de animación se engancha solo a los nodos que lo piden, en vez de
un .animation(nil, value:) en cada nodo del árbol: un an-stack-view juega una
transición con sus pantallas, y un ámbito de animación que se cuele ahí no es de
lo que encuentra un check después. El precio es que encender o apagar [animate]
cambia la rama, y con ella la identidad del nodo, así que el primer fotograma
tras encenderlo no se anima.
La corona digital
Sección titulada «La corona digital»Es el control propio del reloj y el único sin equivalente en un teléfono: analógico, con inercia y con hápticas, y usado sin que un dedo tape la pantalla. Una plantilla la pide como cualquier otro evento:
<an-view (crown)="turned($event)" (crownIdle)="stopped()"> … </an-view>| Clave | Qué es |
|---|---|
delta |
Cuánto ha girado desde el evento anterior. |
offset |
Acumulado desde que la vista tomó la corona. |
velocity |
Con signo, en vueltas por segundo — la velocity de SwiftUI. |
delta no es algo que dé SwiftUI: DigitalCrownEvent lleva offset y
velocity, y el shell resta. Lo que casi siempre quiere una plantilla es «mueve
el valor lo que haya girado», y hacer esa resta en cada plantilla significaría
repetirla en cada plantilla. (crownIdle) llega cuando se para, y es
deliberadamente otra cosa que un (crown) con delta cero — el onIdle de
SwiftUI es un callback aparte y no hay nada que inventar.
El modificador se engancha solo a nodos contenedores, y solo cuando el nodo
escucha crown de verdad.
Tres cosas que conviene saber, y las tres costaron averiguarlas:
- La corona va a quien tiene el foco, y el foco es uno. Eso no es una
decisión del shell, es cómo funciona watchOS. Una vista con
(crown)se declarafocusabley pide el foco inicial condefaultFocus, así que si nadie más lo tiene, se lo queda. Con varias vistas(crown), gana la primera en orden de pintado. - Asignar el
FocusStatea mano no funciona. Se probó desde la raíz y desde elonAppeardel propio nodo: SwiftUI se traga la asignación sin decir nada si la vista todavía no está en pantalla, y la corona se queda muerta sin que nada lo diga. Lo que funciona esdefaultFocus. - Un
ScrollViewse queda la corona. Si la vista con(crown)está dentro de uno, la corona desplaza la lista hasta que se toca la vista. Todas las apps de reloj se comportan así, pero conviene saberlo:examples/watch-controlspone su pantalla de corona fuera de unan-scroll-viewa propósito. Tocar unSlider, unSteppero unPickertambién mueve el foco a ese control, y hay que volver a tocar la vista para recuperar la corona.
Los controles que la usan —an-slider, an-stepper, an-select— la reciben
del sistema sin pedir nada: cuando tienen el foco, la corona los mueve. El
acumulador de la corona se guarda en un diccionario separado del estado de los
controles, porque un mismo nodo puede ser a la vez un an-slider y un oyente de
(crown).
| Gesto | En el reloj |
|---|---|
(press) |
onTapGesture. En un an-button lo envía el propio Button, con su resaltado y sus hápticas. |
(doublePress) |
onTapGesture(count: 2). |
(longPress) |
Un LongPressGesture de medio segundo, en un simultaneousGesture. |
(pan) |
DragGesture, con translation y velocity. Las fases son begin, move y end. |
(swipeLeft), (swipeRight), (swipeUp), (swipeDown) |
El mismo DragGesture, leyendo el balance al soltar: 24 puntos o más en el eje dominante. |
(crown), (crownIdle) |
La corona. |
Solo se engancha lo que pide la plantilla. Un recogniser de más se come el
arrastre del ScrollView de debajo, y watchOS resalta lo que cree que se puede
tocar, así que envolverlo todo en un gesto haría parpadear media pantalla al
rozarla.
(longPress) y el arrastre no pueden ser dos gestos separados. Con
.onLongPressGesture sobre una vista que además escuchaba (swipeLeft), la
pulsación larga no llegaba nunca: los dos pelean por el dedo y ganaba el
arrastre. Va en un simultaneousGesture, y la posición del toque sale del
propio arrastre y no de un segundo DragGesture de distancia cero, que era el
mismo problema otra vez.
Y lo que no llega, avisado una vez por gesto:
| Gesto | Por qué no |
|---|---|
(pinch), (rotation) |
MagnifyGesture y RotateGesture son @available(watchOS, unavailable). Dos dedos no caben en 40 mm. |
(back) |
Fuera de un NavigationStack el reloj no da arrastre de borde, y montar uno metería el layout de SwiftUI dentro del de taffy. |
(refresh) |
En un reloj no se tira de una lista para recargar: eso es la corona, que ya llega como (crown). |
(scroll) |
El ScrollView de SwiftUI no publica su desplazamiento en watchOS 11, que es el mínimo de este shell. |
(safeArea) |
La app es dueña de toda la pantalla y el sistema no reserva ningún margen consultable — así que an-safe-area se monta y no hace nada ahí. |
(focus), (blur) |
Todavía no: el foco del reloj es el mismo foco que decide quién tiene la corona, y dos dueños la harían saltar. |
El estado que mueve un dedo
Sección titulada «El estado que mueve un dedo»Esta es la parte que no se ve y la más fácil de romper. Los dos lados llevan el
mismo valor a distintos ritmos: un Slider de SwiftUI necesita un Binding en
el que pueda escribir de inmediato —el dedo está encima— mientras el valor de
verdad vive en una señal de Angular al otro lado de QuickJS y no vuelve hasta el
siguiente fotograma. Sin algo en medio, el slider saltaría hacia atrás en cada
arrastre, porque cada foto lo devolvería al valor viejo.
Ese algo es AnControls, y tiene una regla:
si el valor que llega de Rust es distinto del que llegó la vez anterior, lo cambió la app, y gana la app. Si es el mismo, gana lo que haya hecho el dedo.
Así una señal que cambia el valor desde código aparece al momento, y un arrastre no lo sobreescribe el eco de su propio evento.
Las presentaciones necesitan lo mismo: pulsar un botón de un an-alert lo
cierra en SwiftUI mientras [visible] sigue siendo true hasta que JS reacciona
al (select), y el fotograma de en medio lo reabriría. El diálogo se anota como
cerrado hasta que la plantilla se pone al día.
Recarga en caliente
Sección titulada «Recarga en caliente»an dev --watchos funciona igual que en el teléfono: la app conecta al servidor
por WebSocket y guardar recarga el bundle sin reiniciar. El simulador de reloj
comparte la red del Mac, así que el bundle se sirve en 127.0.0.1:<puerto>. El
comando no tiene un --device propio — un --device que se haya quedado en el
teléfono por defecto se cambia por el reloj por defecto.
El estado sobrevive, y esa fue la pelea que valía la pena ganar, porque el
árbol se reconstruye entero treinta veces por segundo. Sobrevive porque en una
recarga en caliente el core deja el árbol montado bajo los mismos ids, así que
AnControls no se entera; y cuando la recarga es en frío el host limpia el
árbol, los ids desaparecen, y la reconciliación se lleva por delante lo que ya
no existe. No hay que vaciar nada a mano, que es exactamente donde esto se
habría roto.
Manejar el reloj desde fuera
Sección titulada «Manejar el reloj desde fuera»xcrun simctl no tiene ningún verbo para esto: hay io … screenshot,
io … recordVideo, ui, spawn, push… y ninguno envía un toque ni un giro.
Es la misma situación que el mando del Apple TV, y la salida es la misma que la
de tv-remote.sh: mover el propio ratón del Simulator.
./scripts/watch-input.sh tap 104 200 # un toque, en puntos de reloj./scripts/watch-input.sh hold 104 120 900 # una pulsación larga./scripts/watch-input.sh drag 104 220 104 60 # un arrastre: desplaza una lista./scripts/watch-input.sh turn -12 # doce pasos de corona./scripts/watch-input.sh shot /tmp/a.pngLo que más costó encontrar, y está escrito arriba del script: la rueda del ratón solo gira la corona mientras el puntero está sobre el botón «Crown» de la ventana, no sobre la pantalla. Sobre la pantalla no pasa absolutamente nada —ni un aviso— y es fácil concluir que la corona no se puede manejar desde fuera.
Tiene las mismas dos condiciones que el mando de la tele: la pantalla del Mac no puede estar bloqueada, y el Simulator se queda en primer plano mientras corre el script.
Nada falla en silencio
Sección titulada «Nada falla en silencio»Cuatro avisos, cada uno una vez por caso y nunca una vez por fotograma — a 30 Hz, un aviso por fotograma es un log ilegible:
- Una prop que nadie lee. La lista blanca por tipo está en
reads(), junto al código que la usa;[cursor], para el que solo el host de escritorio tiene un puntero, es una de las props que aterrizan aquí hoy. Los prefijosios:yandroid:yng-versionestán exentos. - Una prop que no se puede pintar, con el motivo, en
unpaintable(). A propósito no es el mismo mensaje que el anterior: «esto todavía no lo lee nadie» es un hueco que alguien puede cerrar y «aquí no hay nada sobre lo que dibujar» no lo es, y quien no pueda distinguirlos se queda esperando una versión que no va a llegar. Los casos que existen hoy son un[borderWidth]o un[rotate]sobre unan-alert: el diálogo lo presenta el sistema y la app le entrega un título, un mensaje y unos botones, no un marco que trazar, mover ni temporizar. - Un evento que no se puede entregar, avisado una vez por tipo y nombre de evento.
- Un primitivo que no se dibuja, con el motivo del SDK.
Las cargas de los eventos son planas y nada más. Un objeto anidado o un array se rechaza con un log, porque el tipo de valor del cable no los puede representar.
Módulos nativos
Sección titulada «Módulos nativos»device está registrado, así que Device.info() resuelve y 'watchos' —un
valor de NativePlatform que hasta entonces no producía ningún host— es un
valor que una app puede leer de verdad.
Los cuatro campos que responde vienen del shell, entregados a
an_watch_runtime_new como JSON junto a los tamaños de los controles:
systemVersion, model y scale son de WKInterfaceDevice, que es WatchKit,
no tiene binding de Rust, y serían cuatro viajes por Objective-C para lo que
Swift resuelve en una línea. Android hace exactamente esto con
AnHost.deviceInfo().
platform es el único campo que el shell no envía. Esa es la palabra del
crate —este host no puede estar corriendo en otro sitio que un reloj— por el
mismo motivo por el que an-ios lo toma del cfg: preguntárselo a otro solo
sería sitio para equivocarse. Si no se entrega nada, la llamada se rechaza
diciéndolo, en lugar de responder un objeto con agujeros que se leería como
datos reales.
Recursos dentro del bundle
Sección titulada «Recursos dentro del bundle»Un <an-image [source]="'logo.png'"> —una ruta sin esquema— es un fichero que
ha viajado con la app, y sale de resources/, en el proyecto: el mismo
directorio que en el teléfono.
an watchos copia el árbol a la raíz del .app, junto a main.js, conservando
los subdirectorios y saltándose los ficheros ocultos.
Ahí es donde los pone iOS también, y no por el mismo motivo. En el teléfono la
raíz es donde busca UIImage(named:); en watchOS esa llamada resuelve nombres
contra un catálogo de assets, y un bundle montado con swiftc a pelo, sin
.xcodeproj, no tiene ninguno. Lo que lo decide aquí es la forma del bundle: un
.app de reloj es plano —WKApplication, sin Contents/—, así que
Bundle.main.resourcePath es el propio .app, que ya es el directorio del que
el shell lee main.js y con el que construye la ruta de una imagen. Un
subdirectorio Resources/ nuestro sería un directorio en el que no mira nada de
Foundation.
Hay cuatro nombres reservados, como en iOS y por el motivo de iOS —el bundle es
plano, así que hasta el ejecutable comparte directorio con los recursos—:
main.js, Info.plist, dev-server.txt y AngularNativeWatch. Un recurso que
fuera a caer sobre uno de ellos para la compilación, diciendo qué fichero. No
hay firma por delante de la que la copia tenga que ir: el simulador del reloj se
queda el bundle sin firmar.
Un [source] que nombra un fichero que no está se avisa una vez, con el
nombre y con el sitio del que tendría que haber salido —la misma frase que dice
an-ios, porque es la misma equivocación—. No puede ser una comprobación de
compilación: solo el dispositivo sabe qué cadenas ha producido de verdad una
plantilla. Y lo que no puede ser es una vista vacía, que en un reloj no se
distingue de una imagen que aún está cargando. examples/watch-controls ya
lleva una, al lado del título de su primera pantalla; hasta que la compilación
no supo llevarla, ese ejemplo no tenía ninguna imagen, porque un ejemplo de este
repositorio no puede depender de que haya un servidor levantado.
Lo que falta
Sección titulada «Lo que falta»- Una vista que trae un plugin. Los métodos de un plugin funcionan aquí:
an-watchtiene el mismo registro que los demás hosts, yan watchossolo rechaza una compilación cuando un plugin dice que no puede cubrir un reloj — mira Plugins en el Mac y en el reloj. Lo que no tiene dónde ir es<an-custom>. Este host refleja el árbol en un modelo que SwiftUI redibuja, así que no hay jerarquía de vistas donde montar la vista de un plugin, y el protocoloAnPlugindel reloj no tieneattachpor el mismo motivo: un sustituto que no entregara nada solo escondería eso hasta tiempo de ejecución. Un nombre de módulo alcanzado en tiempo de ejecución que no está se rechaza con el nombre y el motivo de que no haya nada debajo, que no es el mensaje que recibiría una errata. - Mover un diálogo o una hoja. En todo lo demás ya está hecho:
[animate],[translateX],[scale]y[rotate]son.offset,.scaleEffect,.rotationEffecty.animation(_:value:)sobre el marco que dio taffy, y a SwiftUI no hay que entregarle el estado de partida porque el valor del que viene la vista sigue en el árbol. Unan-alerty unan-modalno tienen ese marco:.alerty.sheetson modificadores sobre la raíz, y el sistema decide de dónde entra la presentación y cuánto tarda. Ahí esas props se rechazan, con el motivo, en vez de tragárselas sin decir nada. - Bordes por lado. No los hay, ni aquí ni en ninguna otra parte del
proyecto.
[borderWidth]es un número y se dibuja;borderTopWidthy sus tres hermanos son estilos de layout — los resuelve taffy, meten hacia dentro a los hijos y no llegan a ningún host como algo que pintar. SwiftUI tampoco tiene una forma para ellos, así que dibujarlos solo en el reloj sería juntar cuatro rectángulos y hacer que el reloj enseñe un borde que el teléfono no enseña. El core lo dice una vez por nombre cuando una plantilla escribe uno, así que se rechaza en voz alta en vez de tragárselo; ver Estilos. - La foto entera en lugar de las mutaciones. Cada fotograma cambiado
serializa el árbol entero. Con diez o quince nodos no se nota. Cuando aparezca
una lista larga habrá que mandar las ops por su cuenta, y lo que cambia es
snapshot.rsy elDecodablede Swift, no el host. - Complicaciones y notificaciones. Otra superficie del sistema, con su propio ciclo de vida. No comparten nada con esto.
- Un dispositivo real. No hay camino de dispositivo por ninguna parte: ni
target
aarch64-apple-watchos, ni firma, nidevicectl. Todo lo de aquí se vio en el simulador, en un Apple Watch Series 11 (46 mm), el dispositivo por defecto.
Accesibilidad, y lo único que no se puede demostrar
Sección titulada «Accesibilidad, y lo único que no se puede demostrar»Los roles, los rasgos y los valores se traducen en Rust y en Swift solo se
enganchan, en cada nodo en lugar de por case. Qué se aplica y qué se rechaza
está en Accesibilidad en Apple.
El hueco que merece decirse aquí es de pruebas: los otros tres hosts de Apple se pueden leer desde fuera recorriendo el árbol de accesibilidad. watchOS no —no hay ninguna ruta hacia el árbol de un simulador de reloj— así que nada demuestra que un lector de pantalla anunciaría nada de esto. El test de Rust solo comprueba que la foto lleva los nombres correctos.
Muros que se esperaban y no estaban
Sección titulada «Muros que se esperaban y no estaban»Documentados porque estaban en la lista de cosas que podían frenar esto:
- El simulador acepta el bundle sin firmar.
simctl installno pide ninguna firma, igual que iOS. Un dispositivo real sí la pediría. @mainsobre unaAppde SwiftUI funciona conswiftcpelado, sin.xcodeproj, siempre que se pase-parse-as-library.- El
Info.plistnecesitaWKApplication. Es lo que distingue una app de watchOS 7 en adelante del viejo par «WatchKit App + WatchKit Extension». Sin esa clavesimctlinstala el bundle y luego no encuentra nada que lanzar. El plist lleva ademásWKWatchOnly—independiente, sin app de iPhone acompañante—,UIDeviceFamily [4]y solo vertical. - La medición del texto es la de verdad. watchOS trae un UIKit recortado sin
UIViewpero conUIFonty el dibujado de cadenas de Foundation, así que el medidor del reloj mide lo mismo que el de iOS y con la misma tabla de pesos: CSS 100..900 mapeado sobre el -0,8..0,62 de UIKit. Fuera del reloj cae al medidor ingenuo para quecargo testcorra en un Mac. Los tamaños de los controles no se pueden preguntar —no hay ningúnUISwitchal que consultar— así que Swift los fija en el código y los pasa al arrancar.