¿Qué hace SwiftUI Experto?
Una pantalla SwiftUI vive o muere por la propiedad del estado: sitúa el estado en el lugar equivocado y cada pulsación de tecla re-evalúa todo el árbol, las listas titubean y las animaciones se rompen. Esta skill construye y revisa vistas SwiftUI de modo que la invalidación se mantenga acotada a las vistas que realmente leen los datos cambiados, el presupuesto de fotograma se mantenga durante el desplazamiento y el resultado sea accesible por defecto en lugar de tener que adaptarlo retrospectivamente.
Procedimiento operativo
Trabaja en este orden porque el diseño del estado determina todo lo demás: arreglar el estado después de construir la jerarquía de vistas implica reescribir la jerarquía.
Paso 1: Recopilar entradas
Recopila antes de escribir cualquier código de vista. Cuando el usuario no lo sepa, usa el valor por defecto y etiquétalo como suposición.
- Dispositivos objetivo y frecuencia de actualización (por defecto: asume iPhone con ProMotion 120Hz más un dispositivo base de 60Hz).
- Forma de los datos: cuántos elementos tiene la lista más grande, con qué frecuencia mutan los datos, si se transmiten en streaming.
- Mapa de propiedad: para cada pieza de estado, qué vista la crea, qué vistas la leen, cuáles la mutan.
- Requisitos de accesibilidad más allá de lo básico (por defecto: soporte completo de Dynamic Type + VoiceOver).
Paso 2: Elegir la propiedad del estado por valor
Selecciona el envoltorio más restrictivo que funcione:
@State: estado local, propiedad de la vista, de tipo valor. También el propietario correcto para una instancia de modelo@Observablecreada por la vista.@Binding: una referencia bidireccional a un estado propiedad de otro lugar. Pasa un binding solo cuando el hijo muta; pasa un valor plano cuando solo lee.@Observable(framework Observation): objetos modelo; las vistas observan solo las propiedades que leen, por lo que la invalidación es por propiedad, no por objeto.@Environment: inyección de dependencias hacia abajo en el árbol para servicios y modelos compartidos.
Regla: el estado reside en el ancestro común más bajo de todas las vistas que lo leen o escriben: ni más abajo (se restablece cuando la vista desaparece) ni más arriba (amplía la invalidación).
Paso 3: Componer vistas para acotar la invalidación
- Mantén
bodypequeño; extrae subvistas para que un cambio invalide una fila, no la pantalla completa. Unbodyque supere aproximadamente las 50 líneas o tres niveles de anidamiento es candidato a dividirse. - Pasa datos mínimos a las subvistas: pasa
item.title, no todo el store, para que solo se re-rendericen las vistas afectadas. - Usa ayudantes
@ViewBuilderen lugar de closures anidados gigantes. - Nunca realices trabajo pesado (formateo, ordenación, filtrado, decodificación) en
body; calcúlalo en el modelo y entrega a la vista los valores ya procesados.bodydebe permanecer en el rango de microsegundos porque puede ejecutarse en cada fotograma durante una animación.
Paso 4: Listas y el presupuesto de fotograma
El presupuesto es de 8.3ms por fotograma a 120Hz, 16.7ms a 60Hz: todo en el hilo principal para ese fotograma, no solo tu fila. Reglas prácticas:
- Usa
ListoLazyVStackpara cualquier cosa por encima de ~50 filas, para que las filas se rendericen bajo demanda; unVStacksimple dentro de unScrollViewconstruye todas las filas de antemano. - Asigna a los elementos
ids estables provenientes de tus datos (item.id), nunca índices de array oUUID()calculados enbody; los ids inestables destruyen la identidad de las filas, causando parpadeo, pérdida de la posición de desplazamiento y pérdida del estado de la fila. - Las filas deben tener costo constante: sin formateadores de fecha ni decodificación de imágenes por fila; almacena en caché los formateadores, usa
AsyncImageo una tubería de imágenes con caché. - Verifica con la plantilla SwiftUI de Instruments: observa los contadores de evaluación de
bodyde las vistas mientras te desplazas. Si elbodyde una fila se evalúa cuando cambia un estado no relacionado, es un error de propiedad del estado: corrige el Paso 2, no apliques memoización alrededor.
Paso 5: Animar desde el estado
- Impulsa la animación desde el estado:
withAnimation { model.expanded.toggle() }. - Prefiere
.animation(_:value:)acotado a un valor específico sobre animaciones globales implícitas, que animan cosas que no pretendías. - Usa
matchedGeometryEffectpara transiciones de elemento compartido;transitionpara insertar/eliminar. - Respeta
accessibilityReduceMotion: reemplaza el movimiento con fundidos de opacidad cuando está activado.
Paso 6: Pasada de accesibilidad
- Añade
accessibilityLabel/accessibilityValue; agrupa elementos relacionados conaccessibilityElement(children: .combine)para que una fila se lea como una oración, no como cinco fragmentos. - Respeta Dynamic Type: estilos de texto del sistema, sin marcos fijos que recorten el texto; verifica en el tamaño de accesibilidad más grande.
- Comprueba el contraste y añade variantes
#Previewpara modo oscuro, Dynamic Type grande y estados vacío/carga/error con datos de ejemplo.
Artefacto trabajado: propiedad del estado, malo vs bueno
Malo: la pantalla completa se invalida con cada pulsación de tecla porque el texto de búsqueda reside en un modelo que todo el árbol observa, y las filas reciben todo el store:
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Buscar", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // la fila observa todo
}
}
}
}
Bueno: el texto de búsqueda es local de la vista, las filas reciben solo los valores que leen, y la lista es perezosa con ids estables:
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}
Escribir en la versión buena invalida el closure de contenido del List y las filas cuya pertenencia filtrada cambió: nada más.
Entregable
Produce código de vista SwiftUI funcional más un mapa de estado de un párrafo que indique, para cada pieza de estado: su propietario, su envoltorio y qué vistas la leen frente a cuáles la escriben. Para revisiones, produce una lista de hallazgos ordenada por impacto en el presupuesto de fotograma, cada uno con su corrección.
NO HAGAS
- Nunca mutar el estado durante la evaluación de
body: provoca bucles o advertencias de comportamiento indefinido. - No hagas trabajo asíncrono en
onAppearcon unTaskcrudo: usa.task {}, que se cancela automáticamente al desaparecer. - No pongas tipos de vista (
Color,Image,AnyView) en los modelos; los modelos deben ser comprobables sin interfaz de usuario. - No eleves todo el estado a un store de nivel de pantalla "por simplicidad": ese es exactamente el patrón que hace que cada pulsación de tecla redibuje la pantalla.
- No disimules errores de invalidación con
equatable()o caché antes de arreglar la propiedad; el error resurgirá en la siguiente vista que observe el mismo objeto. - No uses marcos fijos en el texto: Dynamic Type lo recortará.
Nivel de calidad
Una vista terminada se considera aceptable cuando:
- Cada pieza de estado tiene exactamente un propietario y el envoltorio más restrictivo que funciona.
- Instruments muestra que los
bodyde las filas se evalúan solo cuando cambian sus propios datos. - Desplazarse por la lista más grande esperada mantiene el presupuesto de fotograma en el dispositivo base de 60Hz.
- El estado de larga duración se eleva por encima de cualquier vista que pueda desaparecer, para que sobreviva a la navegación.
- Se gestionan la evitación del teclado y las áreas seguras (
safeAreaInset,scrollDismissesKeyboard). - VoiceOver lee cada elemento interactivo con una etiqueta significativa; el diseño sobrevive al tamaño más grande de Dynamic Type; se respeta
reduceMotion. - Existen previsualizaciones para los estados de carga, vacío, error, modo oscuro y tipo grande.
Para perfilado a nivel de aplicación más allá de una sola pantalla, combínalo con mobile-perf-profiler.