wandres.dev
LISTAS Y COLECCIONES · rendimiento y edición

Secciones, encabezados y estilos de lista

Agrupar contenido con Section, entender que listStyle cambia comportamiento y no solo apariencia, y personalizar filas, fondos y separadores sabiendo dónde hay que aplicar cada modificador y por qué fuera de ese sitio no hace nada.

⏱ 17 min

Una lista plana solo comunica orden; una lista con secciones comunica estructura. Pero agrupar en SwiftUI no es envolver filas en un contenedor cualquiera: Section es un elemento que la maquinaria de la lista reconoce y trata de forma especial, y el estilo elegido decide si sus encabezados se fijan al desplazarse, si el fondo es una tarjeta o una banda, y hasta si la sección puede plegarse. La personalización de filas tiene además una regla de colocación que no es negociable, y casi todos los modificadores que parecen no funcionar están simplemente aplicados en el sitio equivocado.

🎯 Al terminar esta lección sabrás
  • Usar Section con encabezado y pie, y entender qué añade frente a agrupar sin ella.
  • Explicar por qué listStyle cambia comportamiento además de apariencia.
  • Aplicar los modificadores de fila en el nivel correcto del árbol y saber por qué ahí y no dentro.
  • Sustituir el fondo del sistema y los separadores sin romper el resto de la lista.

Secciones como unidad semántica

Section agrupa filas y opcionalmente les pone encabezado y pie. Lo importante es que la lista la reconoce como una unidad real: los separadores se detienen en sus bordes, los índices de accesibilidad la anuncian como grupo y el estilo aplica a su alrededor la decoración que corresponda.

List {
    Section("Pendientes") {
        ForEach(pendientes) { FilaTarea(tarea: $0) }
    }

    Section {
        ForEach(hechas) { FilaTarea(tarea: $0) }
    } header: {
        Label("Completadas", systemImage: "checkmark.circle")
    } footer: {
        Text("Se archivan a los treinta dias.")
    }
}

El pie de sección es la superficie más infrautilizada de una lista. Es el sitio natural para explicar una regla, una consecuencia o una advertencia sin recurrir a un icono de ayuda ni a una alerta, y el sistema lo compone con la tipografía secundaria adecuada en todos los tamaños de texto.

Con varias secciones generadas a partir de datos, la agrupación se convierte en un problema de modelo antes que de vista: necesitas una colección de grupos, cada uno con su propia identidad estable, y dentro de cada grupo un ForEach con la identidad de sus elementos. Anidar identidades bien es lo que permite que insertar una sección entera se anime como tal en lugar de como una reconstrucción.

Agrupar tiene además un efecto que no se ve: el lector de pantalla anuncia el encabezado al entrar en la sección y permite saltar de una a otra con el rotor, lo que convierte una lista larga en algo navegable sin vista. Una lista de cien filas sin secciones es, para ese usuario, un pasillo sin puertas.

Desde iOS 17 las secciones pueden plegarse declarativamente pasando un enlace a un booleano de expansión, lo que traslada el estado de apertura al modelo y lo hace persistible y animable. Cuando ese enlace no está disponible o el estilo no lo soporta, DisclosureGroup cubre el mismo caso con más control visual y menos integración.

Datos agrupados: del modelo a las secciones

Cuando las secciones salen de los datos, la agrupación deja de ser una decisión de vista. Lo que la lista necesita es una colección de grupos con identidad propia, cada uno con su título y sus elementos; calcular eso dentro del cuerpo de la vista, agrupando y ordenando en cada invalidación, es una de las formas más caras de escribir una lista lenta.

struct GrupoTareas: Identifiable {
    let id: String            // por ejemplo, la fecha en formato estable
    let titulo: String
    let tareas: [Tarea]
}

List {
    ForEach(grupos) { grupo in
        Section(grupo.titulo) {
            ForEach(grupo.tareas) { FilaTarea(tarea: $0) }
        }
    }
}

Fíjate en que hay dos niveles de identidad y los dos importan: la del grupo decide si insertar una sección nueva se anima como una inserción o como una reconstrucción de toda la lista, y la de los elementos decide lo mismo dentro de cada grupo. Un identificador de grupo derivado de su índice reproduce, un nivel más arriba, exactamente los fallos de la lección anterior.

flowchart LR
A[Coleccion plana en el modelo] --> B[Agrupar y ordenar fuera del cuerpo]
B --> C[Grupos con identidad estable]
C --> D[ForEach de secciones]
D --> E[ForEach de filas con identidad de elemento]
E --> F[Inserciones y retiradas animadas en el nivel correcto]
style B fill:#f9e2af,color:#11111b
style F fill:#a6e3a1,color:#11111b

listStyle no es solo apariencia

Elegir estilo parece un ajuste cosmético y decide comportamientos observables. El estilo llano fija los encabezados en la parte superior mientras la sección se desplaza; los estilos agrupados los tratan como bloques que se van con el contenido. Los estilos con recuadro dibujan cada sección como una tarjeta con márgenes propios, lo que cambia el ancho útil de la fila y la posición de los separadores. El estilo de barra lateral habilita el plegado y la jerarquía de navegación en iPad y Mac.

📃

plain

Ancho completo, encabezados fijados al desplazarse. El más neutro y el mejor punto de partida cuando el diseño es propio.

🗂️

insetGrouped

Cada sección como tarjeta con márgenes. Es el aspecto de Ajustes y el valor por defecto en iPhone.

📐

inset y grouped

Variantes intermedias: márgenes sin tarjeta, o bandas de ancho completo con separación entre secciones.

🧭

sidebar

Secciones plegables y estética de navegación lateral. Pensado para pantallas grandes, no para una lista de contenido.

Los encabezados fijados merecen un comentario aparte porque son la diferencia funcional más útil entre estilos. Un encabezado que permanece arriba mientras su sección se desplaza mantiene el contexto en listas largas —qué día, qué letra, qué categoría estoy mirando— y por eso el estilo llano es el que usan las agendas y los directorios. Cuando la lista es corta y cada sección cabe en pantalla, fijar el encabezado no aporta nada y el estilo agrupado comunica mejor la separación entre bloques.

El estilo se aplica al contenedor, se propaga por el entorno y afecta a toda la lista, no a una sección suelta. Si necesitas dos apariencias muy distintas en la misma pantalla, la respuesta correcta casi nunca es pelear con el estilo: son dos listas, o es una pila perezosa con tu propia decoración. Conviene también recordar que la prominencia del encabezado se puede aumentar de forma independiente al estilo, lo que resuelve buena parte de los casos en que el título de sección se percibe demasiado tímido.

💡
Elige el estilo antes de personalizar nada

Cada estilo trae márgenes, alturas y reglas de separador propias. Si empiezas ajustando espacios y luego cambias de estilo, tirarás todo el trabajo. Fija primero el estilo, comprueba cómo se ve con texto grande y en modo oscuro, y solo entonces empieza a retocar.

Personalizar filas y separadores

Aquí está la regla de colocación que explica la mayoría de las frustraciones: los modificadores de fila se aplican al contenido de la fila, es decir, al elemento que es hijo directo de la lista o del ForEach. Aplicados a una vista interna no hacen nada, porque la lista solo mira su propio hijo para decidir márgenes, fondo y separadores.

List {
    ForEach(articulos) { articulo in
        FilaArticulo(articulo: articulo)
            .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
            .listRowBackground(articulo.leido ? Color.clear : Color.accentColor.opacity(0.08))
            .listRowSeparator(.hidden)
            .listRowSeparatorTint(.secondary)
    }
    .listSectionSeparator(.hidden, edges: .bottom)
}
.listStyle(.plain)
.listRowSpacing(8)
.scrollContentBackground(.hidden)
.background(Color.fondoApp)

Los márgenes de fila son el modificador que más se malinterpreta. No añaden espacio: sustituyen por completo los márgenes que el estilo había decidido, incluidos los laterales que alinean el contenido con el resto del sistema. Ponerlos a cero para que una imagen ocupe todo el ancho es legítimo; ponerlos a cero por descuido descoloca la alineación de esa fila respecto a todas las demás y respecto a la barra de navegación.

El fondo merece un párrafo propio porque tiene dos capas. El fondo de cada fila se controla con el modificador de fila; el fondo del contenedor lo pinta el sistema y hay que ocultarlo explícitamente antes de poner el tuyo, porque en caso contrario el color propio queda debajo y no se ve. Es el error clásico de quien intenta dar color a una lista y concluye que el modificador de fondo está roto.

El espaciado tiene también dos escalas independientes que suelen confundirse: la separación entre filas dentro de una sección y la separación entre secciones. Ajustar la primera con relleno interno de la fila produce un resultado engañoso, porque el relleno entra dentro del área pulsable y del fondo de fila mientras que el espaciado real queda fuera de ambos. Usar el modificador de espaciado correcto en cada escala evita fondos que se tocan y áreas de toque que se solapan.

Los separadores admiten tres decisiones distintas: ocultar el de una fila, teñirlo, y controlar por separado los de los bordes de sección. Ocultarlos todos y dibujar los tuyos con divisores propios es legítimo y a veces la única salida, pero es también el momento de releer la lección anterior: cuando la personalización llega a reconstruir el separador, el espaciado y el fondo, la lista ya solo aporta el reciclaje y la interacción, y conviene decidir conscientemente si eso sigue compensando.

Un estilo es un contrato de comportamiento disfrazado de apariencia

La lección profunda de esta sección es que en las plataformas de Apple la apariencia y el comportamiento no son capas separables. Cuando eliges un estilo de lista no estás escogiendo un tema visual sobre una mecánica fija: estás escogiendo simultáneamente cómo se fijan los encabezados, dónde empiezan y acaban los separadores, qué ancho tiene realmente la fila, qué gestos tienen sentido y qué anuncia el lector de pantalla. Por eso los sistemas de diseño que tratan el estilo como una capa de pintura acaban produciendo interfaces que se ven bien en una captura y se comportan mal en la mano. Y por eso el camino barato —tomar el estilo cuya mecánica encaja y ajustar lo mínimo— produce mejores resultados que el camino que parece más controlado, que es tomar cualquier estilo y sobrescribirlo todo. Hay además una razón de mantenimiento que suele descubrirse tarde: la decoración del sistema evoluciona con cada versión para acompañar cambios de tipografía, de accesibilidad y de lenguaje visual, y una lista que se apoya en el estilo hereda esas mejoras gratis, mientras que una lista completamente sobrescrita hereda solo la obligación de volver a hacerlo. Personalizar es siempre legítimo; lo que hay que hacer con los ojos abiertos es calcular cuánto de la evolución futura de la plataforma estás renunciando a recibir.

⚔️ Una lista agrupada que sigue siendo tuya
  1. Divide una colección en secciones con identidad propia y comprueba que insertar una sección entera se anima como tal.
  2. Prueba la misma pantalla con cuatro estilos distintos y anota qué cambia en encabezados, márgenes y separadores.
  3. Aplica los modificadores de fila primero dentro de una subvista y luego en el hijo directo, y explica la diferencia observada.
  4. Sustituye el fondo del contenedor por uno propio sin que las filas pierdan su fondo ni su resalte al pulsar.
  5. Oculta los separadores del sistema, dibuja los tuyos y decide de forma razonada si mereció la pena.