Communitygithub.com

gtokman/skills

Cómo SwiftUI decide qué re-renderizar: identidad, ciclo de vida, dependencias y cómo escribir vistas que se actualicen eficientemente, especialmente vistas SwiftUI alojadas en celdas de UIKit (UIHostingConfiguration) y vistas reconstruidas a partir de contenido transmitido o generado.

¿Qué es skills?

Este skill detalla los principios fundamentales del renderizado en SwiftUI: identidad, ciclo de vida y dependencias, basándose en las sesiones WWDC21 'Demystify SwiftUI' y WWDC23 'Demystify SwiftUI performance', además de lecciones verificadas en la práctica. Explica cómo SwiftUI decide qué partes de la interfaz re-renderizar, por qué se pierde estado o se producen parpadeos, y cómo escribir vistas eficientes. Cubre desde reglas básicas de identidad (evitar índices en ForEach, usar IDs estables y únicos) hasta consejos avanzados para dependencias (pasar solo lo necesario a las vistas, usar @Observable correctamente, evitar crear clausuras no comparables en el entorno por cada renderizado). Incluye secciones específicas para SwiftUI dentro de celdas UIKit (UIHostingConfiguration), explicando cómo manejar la reutilización de celdas y el auto-dimensionado con vistas sin tamaño ideal. También proporciona una lista de verificación para revisiones de código, métodos de verificación como `_printChanges()` y el uso de Instruments, y referencias a un caso de estudio real con problemas encontrados y corregidos según estas reglas.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-rendering

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Renderizado de SwiftUI

SwiftUI observa tres cosas en tu código: identidad (¿es este el mismo elemento que antes?), ciclo de vida (cuánto dura esa identidad — y con ella, @State), y dependencias (qué entradas, cuando cambian, hacen que se ejecute body de nuevo). Casi cualquier pregunta del tipo "¿por qué se re-renderizó / perdió estado / animó extrañamente / se atascó?" es una de estas.

El modelo

  • Los valores de las vistas son efímeros; la identidad no lo es. Una estructura de vista se crea, se compara con el valor anterior y se descarta. Lo que persiste es su identidad, y el almacenamiento de @State / @StateObject está vinculado a esa identidad: cuando la identidad cambia, el estado se reemplaza y onAppear/task se ejecutan de nuevo, las imágenes se recargan.
  • Dos tipos de identidad. Explícita: IDs en ForEach, Identifiable, .id(_:). Estructural: el tipo y la posición de una vista en la jerarquía — cada rama de un if/switch es una identidad diferente.
  • El grafo de dependencias. Las dependencias de una vista son sus propiedades almacenadas (entradas de su padre), @State, @Environment, @Binding, y las propiedades @Observable que su body lee. Cuando una cambia, solo las vistas que dependen de ella obtienen un nuevo body. SwiftUI luego compara los nuevos valores hijos con los antiguos y se detiene donde nada cambió.
  • Orden de actualización para una vista invalidada: nuevo valor → propiedades dinámicas actualizadas → body se ejecuta → hijos comparados → recursión.

Reglas

Identidad

  1. Nunca identifiques por índice (ForEach(Array(x.enumerated()), id: \.offset), 0..<count sobre datos dinámicos). Cualquier cosa insertada arriba desplaza cada índice posterior: su estado se reinicia, sus imágenes se recargan, las animaciones van al lugar equivocado. Usa un ID estable y único.
  2. Los IDs deben ser estables y únicos. No uses UUID() en un id calculado (nueva identidad en cada renderizado). No uses nombres que se puedan repetir (dos golosinas "Galleta de perro"). Desambiguiza repeticiones (clave, clave#2) si los datos no pueden garantizarlo — los IDs duplicados en un ForEach se comportan mal.
  3. Para contenido generado o transmitido, deriva la identidad de una estructura que no se mueva: dónde comienza el elemento en el texto fuente (la transmisión solo agrega), o posición de la plantilla + clave por iteración para la salida del programa (modelo de React: plantilla:clave). Nombra elementos sin clave (texto suelto, párrafos) después del elemento clave anterior más un contador (e812+2), para que el nuevo contenido al final nunca renombre elementos anteriores.
  4. Prefiere una vista con modificadores cambiantes sobre ramas. if vencido { v.opacidad(0.3) } else { v } son dos identidades; v.opacidad(vencido ? 0.3 : 1) es una. Los valores "inertes" (opacidad 1, padding 0, frame sin tamaños, layoutPriority(0), transformEnvironment) cuestan ~nada. Usa una rama solo para vistas genuinamente diferentes.
  5. Cambia diseños con AnyLayout, no con if envolver { FlowLayout { … } } else { HStack { … } } — los hijos mantienen su identidad y estado.
  6. Evita AnyView. Oculta la estructura de tipos a SwiftUI (peor comparación, y en List/Table obliga a construir cada fila para contarlas). Usa funciones @ViewBuilder, switch, genéricos.
  7. Modela los tipos de elementos como un enum y haz switch sobre él en la vista que despacha, sin default: un nuevo caso no puede ser ignorado silenciosamente.

Dependencias

  1. Pasa a las vistas solo lo que usan. Una vista que toma un modelo completo ejecuta body de nuevo cuando cualquier campo cambia; pasa la imagen, no el perro. Extrae subvistas para delimitar dependencias — con criterio, no en cada línea.
  2. @Observable rastrea lo que body lee. Leer una propiedad en un cierre de acción no crea dependencia; leerla en body sí. Pon el estado que la vista muestra en el modelo y léelo en body solo donde sea necesario.
  3. Nunca pongas un nuevo cierre (u otro valor no comparable) en el entorno por cada renderizado — p. ej. .environment(\.acciones, Acciones { … }) en un body. No se puede comparar, por lo que cada lector se invalida en cada actualización del padre. Pon un objeto estable en el entorno en su lugar (.environment(modelo) con @Observable, leído como @Environment(Modelo.self) var modelo: Modelo?), y llámalo desde las acciones.
  4. No notifiques cuando nada cambió. Una propiedad @Observable notifica en cada asignación. Protege: if nuevo != valor { valor = nuevo } — importa cuando reconstruyes estado derivado en cada fragmento o tick.
  5. Vistas Equatable para saltar subárboles sin cambios. Conforma la vista (solo posible cuando no tiene envoltorios de propiedad) y aplica .equatable() donde un padre se re-renderiza a menudo con hijos mayormente sin cambios (una lista de bloques mientras el contenido se transmite). Escribe un == personalizado que compare solo lo que la vista muestra. ⚠️ .equatable() oculta el layoutPriority del hijo de su contenedor. Dentro de un HStack/fila, un hijo con layoutPriority(1) envuelto en EquatableView deja de ganar espacio sobrante y la fila se distribuye de manera diferente. Usa .equatable() en listas verticales de bloques, no en hijos de filas/pilas que dependan de la prioridad.

body barato

  1. Sin análisis, decodificación, filtrado, ordenamiento o trabajo pesado de asignación en body. Se ejecuta en cada invalidación. Precalcula en el modelo, almacena en caché (un NSCache con clave por entrada), o muévelo a la vista cuyas entradas realmente cambian.
  2. Separa las entradas costosas del estado que cambia rápidamente. Si una vista decodifica datos y mantiene @State que cambia a menudo (arrastrar, desplazar, pasar el cursor), la decodificación se vuelve a ejecutar en cada cambio. Decodifica en un padre, pasa el resultado a un hijo que posea el @State.
  3. El trabajo lento va en .task, no en init o body. Hazlo async; nunca bloquees en el valor inicial de una propiedad dinámica.
  4. Almacena en caché el formateo costoso — AttributedString(markdown:), interpolación de cadenas en rutas calientes, búsquedas en paquetes.

Listas

  1. List/Table recopila todos los IDs por adelantado. Mantén la generación de IDs barata, y dale a cada elemento de ForEach un número constante de vistas — sin filtros if o AnyView dentro del cierre de la fila (filtra los datos, en el modelo, no en línea en body). ForEach anidado está bien para secciones.

SwiftUI en celdas UIKit (UIHostingConfiguration)

  • Las celdas se reutilizan, y el estado de la vista SwiftUI puede trasladarse al siguiente elemento mostrado en esa celda. Dale a la raíz .id(item.id) para que un elemento diferente obtenga un árbol nuevo — y mantén el estado visible para el usuario (marcado, expandido, selección) en el modelo, con clave por IDs estables, no en @State, o se pierde/aplica incorrectamente en la reutilización y el desplazamiento.
  • Reconfigurar una celda reconstruye el valor de la vista raíz. Si el contenido está impulsado por un modelo observado, haz que la vista raíz sea Equatable en lo que muestra (no en entradas sin procesar que solo alimentan el modelo) y aplica .equatable(), para que la reconfiguración por actualización no vuelva a ejecutar todo el árbol — la observación hace la actualización real.
  • Auto-dimensionado se mide con una altura abierta. Las vistas sin un tamaño ideal colapsan a cero allí: Canvas, formas, GeometryReader. .aspectRatio(r, contentMode: .fit) no es suficiente — dales un tamaño con un pequeño Layout personalizado (ancho de la propuesta, altura = ancho / relación; ignora alturas de prueba).
  • .aspectRatio(nil, contentMode:) fija una vista al tamaño ideal de su contenido — las tarjetas dejan de llenar su fila. Aplica .aspectRatio solo cuando tengas una relación.

Lista de verificación para revisión

  • Cada ForEach tiene IDs estables y únicos — sin \.offset, sin UUID() calculado.
  • Sin AnyView; ramas solo para vistas genuinamente diferentes; AnyLayout para cambios de diseño.
  • Las vistas reciben solo los datos que renderizan.
  • No se crea nada no comparable por renderizado en el entorno.
  • body no realiza análisis/decodificación/filtrado; entradas costosas separadas del @State rápido.
  • Las propiedades observables no se reasignan a valores iguales.
  • .equatable() donde ahorra trabajo — y no en hijos que usen layoutPriority.
  • Celdas alojadas: raíz .id(item.id), estado visible en el modelo, raíz Equatable.
  • A las vistas sin un tamaño ideal se les da uno antes de ponerlas en celdas de auto-dimensionado.

Verificación

  • let _ = Self._printChanges() al inicio de un body registra por qué se ejecutó (@self, @identity, o la propiedad que cambió). Solo para depuración — elimínalo antes de enviar.
  • Instruments → plantilla SwiftUI para conteos de body y actualizaciones lentas; Hangs/Hitches para los efectos.
  • Prueba de identidad: renderiza cada prefijo del contenido transmitido y afirma que cada ID de elemento completado aparece sin cambios en el renderizado final; después de interacciones, afirma que los elementos supervivientes mantuvieron sus IDs; afirma que no hay IDs de hermanos duplicados.
  • Diferencia de píxeles antes/después de una refactorización de renderizado: renderiza vistas fuera de pantalla en macOS con ImageRenderer (mismo código, .frame(width:), proposedSize con altura nil) y compara los PNG. Detecta regresiones de diseño como la de EquatableView/layoutPriority. (Map, ScrollView e imágenes remotas no se renderizan fuera de pantalla; compara todo lo demás). Divide una diferencia alternando un archivo cambiado a la vez.

Referencias

  • references/case-study.md — el renderizador de respuestas AddToChatMenu (marcado generado por modelo transmitido en celdas de collection view): cada problema encontrado contra estas reglas y su solución.
  • WWDC21 Demystify SwiftUI (sesión 10022); WWDC23 Demystify SwiftUI performance (sesión 10160).

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills relacionados