wandres.dev
UIKIT · lo que sigue vivo

UICollectionView moderno: composición e identidad

La reforma de 2019 convirtió la API más temida de UIKit en la mejor diseñada del framework: el layout dejó de subclasearse para describirse y los datos dejaron de indexarse para identificarse. Esta lección reconstruye la jerarquía composicional de item, grupo y sección, explica por qué una fuente de datos diferenciable resuelve una clase entera de errores por construcción, y argumenta por qué ambas ideas siguen siendo la referencia incluso para quien trabaja en SwiftUI.

⏱ 19 min

Hay un experimento mental que ordena esta lección entera: si Apple hubiera diseñado UICollectionView en 2019 en lugar de en 2012, se parecería mucho más a SwiftUI de lo que se parece a UIKit. Y en cierto sentido eso fue justamente lo que ocurrió, porque la reforma presentada aquel año no añadió funciones a la API vieja sino que sustituyó sus dos partes más dolorosas por dos ideas declarativas puras. El layout dejó de ser una clase que se subclasea calculando atributos a mano y pasó a ser una descripción compuesta de elementos, grupos y secciones. Los datos dejaron de manipularse por índices con inserciones y borrados coordinados a mano y pasaron a expresarse como una instantánea de identificadores que el sistema compara con la anterior para deducir la animación. El resultado es una API que sigue siendo referencia siete años después, que resuelve problemas que SwiftUI todavía no cubre con la misma solvencia, y cuyas dos ideas centrales son exactamente las que uno reconoce, ya interiorizadas, en el modelo declarativo.

🎯 Al terminar esta lección sabrás
  • Componer layouts con la jerarquía de elemento, grupo, sección y entorno, incluidos el desplazamiento ortogonal y los elementos suplementarios.
  • Modelar datos con instantáneas de identificadores y entender qué clase de errores desaparece al hacerlo.
  • Distinguir recargar de reconfigurar y elegir la operación correcta para cada cambio.
  • Situar la API frente a las colecciones de SwiftUI y saber cuándo sigue siendo la elección correcta.

De subclasear un layout a describirlo

Conviene recordar el punto de partida, porque sin él la reforma parece un capricho de estilo. Antes de 2019, cualquier disposición que no fuera una rejilla uniforme exigía escribir una subclase de layout y responder a mano a cuatro preguntas: cuál es el tamaño total del contenido, qué atributos tiene el elemento en cada índice, qué elementos caen dentro de un rectángulo visible y qué debe invalidarse cuando cambia algo. Ese código era largo, difícil de probar, imposible de componer y una fuente inagotable de errores de rendimiento, porque la tercera pregunta se contesta miles de veces por segundo durante el desplazamiento.

El diseño composicional elimina la subclase por completo y la sustituye por una expresión que se construye de dentro hacia fuera. Un elemento describe un hueco; un grupo describe cómo se disponen varios huecos en una línea horizontal o vertical; una sección envuelve un grupo y le añade márgenes, elementos suplementarios y comportamiento de desplazamiento; y el layout final es una función que, dado el índice de sección y el entorno, devuelve la sección correspondiente.

flowchart TB
a[NSCollectionLayoutSize con dimensiones fraccionales absolutas o estimadas] --> b[NSCollectionLayoutItem]
b --> c[NSCollectionLayoutGroup horizontal o vertical]
c --> d[NSCollectionLayoutSection con margenes y suplementarios]
d --> e[UICollectionViewCompositionalLayout como funcion de seccion y entorno]

La pieza que hace componible todo el sistema es el tipo de dimensión, que admite cuatro formas: fracción de la anchura del contenedor, fracción de la altura, valor absoluto en puntos y valor estimado que el sistema ajustará midiendo el contenido real. La cuarta es la que trajo el dimensionado automático a las colecciones sin una línea de cálculo, y su coste conviene conocerlo: una estimación muy alejada del valor real obliga a recalcular durante el desplazamiento y produce saltos en la barra de scroll.

// Un carrusel horizontal dentro de una lista vertical, sin subclases
func seccionCarrusel() -> NSCollectionLayoutSection {
    let elemento = NSCollectionLayoutItem(layoutSize: .init(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(1)))

    let grupo = NSCollectionLayoutGroup.horizontal(layoutSize: .init(
        widthDimension: .fractionalWidth(0.85),
        heightDimension: .absolute(220)), subitems: [elemento])

    let seccion = NSCollectionLayoutSection(group: grupo)
    seccion.orthogonalScrollingBehavior = .groupPagingCentered
    seccion.interGroupSpacing = 12
    return seccion
}

El desplazamiento ortogonal que aparece en ese fragmento merece una mención aparte, porque es la funcionalidad que más trabajo ahorraba antes de existir. Permite que una sección se desplace horizontalmente dentro de una colección vertical con paginación, centrado o desplazamiento continuo, reutilizando celdas correctamente y sin anidar una colección dentro de otra, que era la solución clásica y una fuente crónica de problemas de reutilización y de gestos en conflicto.

💡
El entorno de layout es donde vive la adaptabilidad

La función que produce cada sección recibe un entorno con la anchura del contenedor y las características del dispositivo. Esa es la manera idiomática de hacer un layout adaptativo: en lugar de escribir código condicional repartido por la pantalla, se decide dentro de la función cuántas columnas caben según la anchura recibida. Es el mismo razonamiento que en SwiftUI se hace con clases de tamaño y lectores de geometría, pero concentrado en un único punto.

Identidad en lugar de índices

La segunda mitad de la reforma es más profunda que la primera porque cambia el modelo de datos, no solo la sintaxis. La fuente de datos clásica respondía preguntas por índice y exigía que cada mutación se comunicase con una operación por lotes en la que el número de elementos antes y después tenía que cuadrar exactamente con las inserciones y borrados declarados. Cuando no cuadraba, la aplicación no fallaba silenciosamente: se caía con una excepción, y ese fallo era uno de los más comunes de la plataforma.

La fuente diferenciable elimina esa categoría entera de errores por construcción. En lugar de decir qué operaciones se realizaron, se declara cuál es el estado actual mediante una instantánea que contiene identificadores de sección y de elemento, y el sistema calcula por diferencia qué animaciones corresponden. El paralelo con el modelo declarativo es exacto: se describe el resultado y el motor deduce la transición.

enum Seccion: Hashable { case destacados, recientes }

var instantanea = NSDiffableDataSourceSnapshot<Seccion, Articulo.ID>()
instantanea.appendSections([.destacados, .recientes])
instantanea.appendItems(destacados.map(\.id), toSection: .destacados)
instantanea.appendItems(recientes.map(\.id), toSection: .recientes)
await fuente.apply(instantanea, animatingDifferences: true)

Hay un detalle de diseño en ese código que decide si la API funciona o si produce bugs sutiles, y conviene subrayarlo: la instantánea contiene identificadores, no modelos. La tentación de meter el modelo completo es fuerte y el compilador la permite si es conformante a la comparación por igualdad y al cálculo de resumen, pero la consecuencia es que cualquier cambio en un campo produce un identificador distinto, el sistema lo interpreta como un borrado seguido de una inserción, y la fila desaparece y reaparece con animación en lugar de actualizarse en su sitio.

⚠️
Identificadores estables, únicos y baratos

Tres propiedades no negociables. Estables: el mismo elemento conserva su identificador entre instantáneas aunque su contenido cambie. Únicos: un identificador repetido en dos secciones produce comportamiento indefinido y a menudo un fallo en tiempo de ejecución. Baratos: el cálculo de resumen se ejecuta sobre todos los elementos en cada aplicación, así que un resumen que recorra cadenas largas se nota en colecciones grandes.

Desde iOS 15 existe una distinción que mucha gente ignora y que tiene efecto directo en la fluidez. Recargar un elemento destruye la celda y crea otra; reconfigurarlo conserva la celda existente y vuelve a aplicarle su configuración de contenido. Para el caso habitual —un contador que cambia, un estado que pasa de pendiente a hecho— reconfigurar es correcto, es notablemente más barato y evita el parpadeo. Recargar solo tiene sentido cuando la celda debe cambiar de tipo o de estructura.

El complemento de 2020

Un año después de la reforma llegó la pieza que cerró el círculo y que explica por qué UITableView dejó de tener razón de ser. La configuración de lista permite que una sección composicional adopte la apariencia y el comportamiento de una tabla nativa —separadores, estilos agrupados e insertados, acciones de deslizamiento, celdas expandibles en esquema— sin abandonar la colección. A partir de ahí, cualquier layout es expresable con una sola clase de vista.

📋

Configuracion de lista

Convierte una sección en una tabla con apariencia nativa y acciones de deslizamiento. Es la razón por la que hoy no hay motivo para empezar una pantalla nueva con UITableView.

🧾

Registro de celdas

Sustituye el registro por identificador de cadena y el descarte con conversión de tipo por un objeto genérico que construye y configura la celda en un solo punto, con tipos comprobados por el compilador.

🧩

Configuracion de contenido

Separa el aspecto del contenido de la celda que lo aloja. La celda deja de tener subvistas propias y pasa a recibir una descripción de contenido y otra de fondo, que es una idea declarativa dentro de una clase imperativa.

Merece atención que esas tres piezas empujan en la misma dirección: reducir la superficie mutable de la celda hasta convertirla en una función de su configuración. Una celda moderna bien escrita no tiene métodos de actualización ni estado interno; recibe una configuración y la aplica. Quien haya escrito vistas de SwiftUI reconoce el patrón de inmediato, y esa convergencia no es casual sino el resultado de que ambos equipos llegaron a la misma conclusión sobre cómo debe comportarse una celda reutilizable.

Por qué sigue siendo la referencia

Queda la pregunta que importa a alguien que trabaja en declarativo: si SwiftUI tiene List, LazyVGrid y contenedores de desplazamiento con toda la maquinaria moderna, para qué sirve conocer esto. Hay tres respuestas y ninguna es nostálgica.

La primera es de cobertura. Layouts genuinamente irregulares —secciones con reglas distintas, elementos que ocupan varias columnas, mosaicos con proporciones variables, desplazamiento ortogonal con paginación centrada— siguen siendo mucho más directos de expresar con la jerarquía composicional que con rejillas perezosas, donde a menudo hay que simular la irregularidad con contenedores anidados y cálculos manuales.

La segunda es de escala. En colecciones muy grandes con actualizaciones frecuentes, la combinación de reutilización explícita, prefetch y reconfiguración selectiva ofrece un control fino sobre el trabajo por cuadro que el modelo declarativo administra por ti y, cuando decide mal, no expone palanca para corregirlo. No es un argumento contra SwiftUI, es un argumento sobre dónde está el techo.

La tercera es conceptual y es la que más rendimiento intelectual da. Las dos ideas de la reforma —describir el layout en lugar de calcularlo, y describir el estado por identidad en lugar de por operaciones— son precisamente las ideas sobre las que se construyó SwiftUI. Estudiarlas en un entorno donde son opcionales y donde se ve el mecanismo enseña más que usarlas en un entorno donde son obligatorias e invisibles.

Diferenciar por identidad es la idea que sostiene toda la interfaz declarativa moderna

Vale la pena detenerse en lo que realmente ocurrió cuando Apple sustituyó las operaciones por lotes por instantáneas, porque es el mismo movimiento que hizo la industria entera y casi nunca se cuenta así. El modelo antiguo obligaba al programador a mantener dos representaciones sincronizadas a mano: el modelo de datos, que él controlaba, y la creencia de la vista sobre ese modelo, que solo se actualizaba si él comunicaba correctamente cada diferencia. Toda la clase de errores asociada —las excepciones por recuentos que no cuadran, las celdas que muestran datos de otra fila, las animaciones absurdas— no eran fallos de programadores descuidados, eran la consecuencia inevitable de exigir que un humano calcule un diferencial a mano en un sistema concurrente donde los datos llegan de la red en cualquier momento. La solución es siempre la misma y aparece por todas partes: desplazar el cálculo del diferencial de la persona a la máquina. Es exactamente lo que hizo el DOM virtual en la web, lo que hace la reconciliación de SwiftUI entre dos árboles de vistas, lo que hace un sistema de control de versiones al comparar dos árboles de archivos, y lo que hace un motor de infraestructura declarativa al comparar el estado deseado con el observado. En los cuatro casos el patrón es idéntico: el ser humano declara qué debe haber, la máquina deduce qué hay que hacer para llegar hasta ahí, y a cambio de una restricción —hace falta una noción sólida y estable de identidad, porque sin identidad no hay diferencial posible— se elimina una familia entera de errores en lugar de corregirlos uno a uno. Por eso el requisito de identificadores estables no es un detalle burocrático de esta API concreta: es el precio conceptual del modelo declarativo, y es la misma razón por la que en SwiftUI un identificador mal elegido en un bucle destruye el estado de las filas. Quien entiende esto en UICollectionView, donde el mecanismo está a la vista, deja de tener problemas con la identidad en cualquier framework declarativo.

📝
Lo esencial

Dos reformas independientes y ambas declarativas. El layout composicional sustituye la subclase por una descripción de elemento, grupo, sección y entorno, con dimensiones fraccionales, absolutas o estimadas y con desplazamiento ortogonal integrado. La fuente diferenciable sustituye las operaciones por lotes por instantáneas de identificadores, y elimina por construcción la familia de fallos por recuentos descuadrados. Los identificadores deben ser estables, únicos y baratos, y nunca deben ser el modelo completo. Reconfigurar no es recargar. Y las dos ideas son, literalmente, las que sostienen SwiftUI.

⚔️ Construir la colección que SwiftUI te complicaría
  1. Implementa una pantalla con tres secciones heterogéneas: un carrusel con paginación centrada, una rejilla de dos columnas y una lista con acciones de deslizamiento, todo en una sola colección.
  2. Haz que el número de columnas de la rejilla dependa de la anchura recibida en el entorno de layout y verifícalo rotando y en modo multitarea de iPad.
  3. Sustituye el modelo completo por su identificador en la instantánea y observa la diferencia de animación al cambiar un campo de un elemento existente.
  4. Compara con Instruments el coste de recargar un elemento frente al de reconfigurarlo en una colección con varios miles de elementos.
  5. Reproduce la misma pantalla con contenedores perezosos de SwiftUI y anota exactamente qué te costó más, qué te costó menos y qué no pudiste reproducir.