Una lista puede mostrar diez elementos y a la vez estar evaluando contenido de muchos más. Al investigar ese comportamiento, conviene revisar qué devuelve el bloque de ForEach. Una condición aparentemente inocente puede complicar el trabajo que SwiftUI necesita hacer antes de presentar las filas.
SwiftUI dispone de un diagnóstico específico para encontrar estas situaciones. Abre el esquema de la aplicación en Xcode, entra en Run → Arguments → Arguments Passed On Launch y añade:
-LogForEachSlowPath YES
Ejecuta la aplicación y abre la pantalla que quieres analizar. Este argumento, recogido en la documentación de ForEach, activa mensajes cuando SwiftUI encuentra un número no constante de vistas por elemento en contenedores como List y LazyVStack.
El detalle está en esa expresión: vistas por elemento. Que el array tenga más o menos registros es normal. El problema aparece cuando un registro aporta una fila y otro ninguna, o cuando unos aportan una vista y otros dos. El contenedor necesita resolver esa estructura para organizar su contenido.
Imagina una aplicación para gestionar las herramientas de un taller. Cada herramienta tiene un identificador persistente, un nombre, las unidades disponibles y una posible advertencia de mantenimiento:
import SwiftUI
struct WorkshopTool: Identifiable {
let id: UUID
var name: String
var availableUnits: Int
var maintenanceWarning: String?
}
Para mostrar únicamente las herramientas disponibles, resulta tentador escribir lo siguiente dentro de la vista:
List {
ForEach(tools) { tool in
if tool.availableUnits > 0 {
Text(tool.name)
}
}
}
Cada herramienta produce cero o una fila. Apple explica en Demystify SwiftUI performance que List obtiene los identificadores de sus filas por adelantado. Cuando la cantidad de filas depende del contenido, necesita evaluar ese contenido para resolverlos. Esto añade trabajo incluso para elementos que todavía no son visibles.
Si la intención es excluir herramientas, prepara la colección antes de construir las filas:
struct AvailableToolsList: View {
let availableTools: [WorkshopTool]
var body: some View {
List {
ForEach(availableTools) { tool in
Text(tool.name)
}
}
}
}
La capa que prepara los datos puede obtener esa colección con tools.filter { $0.availableUnits > 0 }. Cada elemento recibido por la vista tendrá ahora una fila. Si el inventario cambia, también debe actualizarse la colección filtrada.
El filtrado sigue teniendo un coste. Moverlo a una propiedad calculada no lo convierte en un resultado almacenado: se ejecutará cada vez que se acceda a ella. Para colecciones grandes o actualizaciones frecuentes, Apple recomienda preparar y conservar el resultado en el modelo, recalculándolo cuando cambien los datos relevantes. Así evitas repetir el recorrido en cada evaluación de body.
El mensaje de diagnóstico puede sugerir encapsular el contenido en un VStack, pero hay que interpretar esa recomendación. Como señala Natalia Panferova en su artículo, envolver la condición que excluye elementos mantiene una fila vacía por cada elemento descartado. En nuestro inventario, eso conservaría una fila para herramientas sin unidades. Si quieres eliminar filas, filtra los datos.
El caso cambia cuando todas las herramientas deben aparecer, pero algunas necesitan información adicional. Por ejemplo, este bloque aporta uno o dos textos directamente al contenedor:
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(tools) { tool in
Text(tool.name)
if let warning = tool.maintenanceWarning {
Text(warning)
.foregroundStyle(.orange)
}
}
}
}
Aquí sí tiene sentido agrupar. Las pilas diferidas necesitan conocer cuántas subvistas aportan los elementos anteriores para acceder a ellas mediante índices. Una cantidad variable obliga a resolver el contenido para determinar esas posiciones.
ScrollView {
LazyVStack(alignment: .leading, spacing: 16) {
ForEach(tools) { tool in
VStack(alignment: .leading, spacing: 4) {
Text(tool.name)
.font(.headline)
if let warning = tool.maintenanceWarning {
Text(warning)
.font(.caption)
.foregroundStyle(.orange)
}
}
}
}
}
Ahora cada herramienta aporta un VStack. La advertencia sigue siendo opcional y la altura puede variar; lo constante es la cantidad de vistas que recibe directamente la pila exterior. Además, el ejemplo separa dos decisiones visuales: 16 puntos entre herramientas y 4 entre el nombre y su advertencia. El contenedor añadido también organiza el contenido, así que su alineación y espaciado deben encajar con el diseño.
Que desaparezca el aviso confirma que has corregido esa estructura, pero no demuestra por sí solo que la pantalla se cargue de forma ágil. Para comprobar el impacto, reproduce la misma apertura y el mismo desplazamiento antes y después del cambio, con el mismo conjunto de datos.
Apple muestra este proceso en Optimize SwiftUI performance with Instruments. Con Product → Profile puedes registrar la interacción usando la plantilla SwiftUI. Revisa las actualizaciones largas de body y apóyate en Time Profiler para localizar el trabajo que consume más tiempo. El gráfico de causas y efectos también ayuda a investigar actualizaciones innecesarias si el problema continúa.
Para revisar un ForEach, empieza por una decisión concreta: si un elemento no debe aparecer, exclúyelo de la colección; si debe aparecer con contenido opcional, dale una estructura exterior estable. Después mide el resultado. Esa combinación permite conservar la intención de la interfaz y comprobar si has eliminado el trabajo que estaba frenándola.