SwiftUI – List
Qué es List
El componente List
permite mostrar un listado vertical de elementos. A diferencia del VStack
estos elementos ocupan todo el ancho del List
y la carga de las vistas es lazy
. Esto nos permite mejorar mucho el rendimiento, ya que el sistema solo cargará en memoria aquellos elementos visibles en pantalla y eliminará de memoria aquellos que dejen de ser visibles. De esta forma se consigue una manejo de memoria muy eficiente. Es un componente equivalente a UITableView
de UIKit
. Puedes consultar aquí la documentación oficial.
El componente List
tiene varios inicializadores que se usarán dependiendo del tipo de lista y de cómo tengamos los datos a mostrar. En su forma más básica encontramos el siguiente caso:
List { ForEach(0..<20) { Text("Index: ($0)") } }
Este es el caso más común en el que crearemos dentro del bloque ViewBuilder
todas las vistas que necesitemos en base a la fuente de datos. Recuerda que para crear un bucle dentro de un ViewBuilder
se debe usar el componente ForEach
definido en SwiftUI
para estos casos. Al hacerlo de esta forma podemos usar el componente Section
dentro de List
para crear secciones con un header
y/o footer
englobando los datos dentro de cada sección:
List { Section(header: Text("First Header"), footer: Text("First footer")) { ForEach(0..<10) { Text("Index: ($0)") } } Section(header: Text("Second Header"), footer: Text("Second footer")) { ForEach(10..<20) { Text("Index: ($0)") } } } .listStyle(InsetGroupedListStyle())
También tenemos otros inicializadores donde podemos incluir más parámetros para crear, por ejemplo, una lista colapsable:
struct RowItem: Identifiable { let id = UUID() var name: String var list: [RowItem]? static let mockupItems: [RowItem] = { var item1 = RowItem(name: "Section 1", list: [RowItem(name: "Option 1"), RowItem(name: "Option 2"), RowItem(name: "Option 3"), RowItem(name: "Option 4"), RowItem(name: "Option 5") ]) var item2 = RowItem(name: "Section 2", list: [RowItem(name: "Option 6"), RowItem(name: "Option 7"), RowItem(name: "Option 8"), RowItem(name: "Option 9"), RowItem(name: "Option 10") ]) return [item1, item2] }() } struct ListSimpleCollapsableView: View { let items: [RowItem] = RowItem.mockupItems var body: some View { List(items, children: .list) { children in Text(children.name) } } }
Para este caso los listados de tipo RowItem
que recibe el inicializador deben implementar el protocolo Identifiable
, que necesita que cada elemento tenga una propiedad id
que debe ser única para que el listado pueda identificar cada row. Al inicializar de esta forma el List
perdemos la capacidad de poder usar el componente Section
.
Modificadores comunes para List
El componente List
comparte los mismos métodos de personalización que el componente View
y pueden ser consultados en el siguiente enlace
listStyle
Permite modificar el estilo visual de presentación de las celdas de List
. El parámetro de entrada debe implementar el protocolo ListStyle
. La SDK nos proporciona varias implementaciones que cambian el aspecto visual del componente:
PlainListStyle
List { ForEach(0..<30) { Text("Index: ($0)") } } .listStyle(PlainListStyle())
InserListStyle
List { ForEach(0..<30) { Text("Index: ($0)") } } .listStyle(InsetListStyle())
GroupListStyle
List { ForEach(0..<30) { Text("Index: ($0)") } } .listStyle(GroupedListStyle())
InsetGroupListStyle
List { ForEach(0..<30) { Text("Index: ($0)") } } .listStyle(InsetGroupedListStyle())
SidebarListStyle
List { ForEach(0..<30) { Text("Index: ($0)") } } .listStyle(SidebarListStyle())
listRowBackground
Este modificador se debe aplicar a las celdas del componente List
. Si se aplica sobre List
no tiene ningún efecto. Permite modificar el color de fondo de toda la celda. Para este caso el modificador background
no nos sirve ya que pondría el color de fondo al componente pero no a la celda.
struct ContentView: View { var body: some View { List { ForEach(0..<30) { cell(index: $0) } } } func cell(index: Int) -> some View { VStack(alignment: .leading) { Text("Index") .font(.caption) Text("(index)") .font(.title2) } .listRowBackground((index % 2 == 0) ? Color.blue.opacity(0.3) : Color.orange.opacity(0.3)) } }
Cómo entrar en modo edición (EditMode) en un List
SwiftUI
proporciona una funcionalidad por la cual se puede indicar a los componentes visuales que están en modo edición. Esto hará que se modifique su comportamiento permitiéndonos realizar nuevas acciones sobre los componentes. Para ello hay que modificar el valor de la variable de entorno EditMode
. Esta variable se puede modificar de dos formas:
- A través del botón
EditButton
que proporcionaSwiftUI
. Con este botón se podrá realizar el control del modo de edición de forma automática, ya que implementa todo lo necesario para entrar y salir de él. El botón se puede añadir a cualquier parte de la pantalla.
.navigationBarItems(trailing: EditButton())
A continuación vemos un ejemplo donde a través de este botón se permite eliminar celdas del List
.
struct ContentView : View { @State private var items = ["Iron Man", "The Incredible Hulk", "Iron Man 2", "Thor", "Captain America", "Marvel's The Avengers"] var body: some View { NavigationView { List { ForEach(items, id: .self) { Text($0) } .onDelete { items.remove(atOffsets: $0) } } } .navigationBarItems(trailing: EditButton()) } }
- Modificando la variable de entorno
editMode
manualmente y controlando sus diferentes estados. Es lo mismo que con el botónEditButton
pero nos debemos encargar nosotros de actualizar el estado de la variable de entornoeditMode
. Esta variable se modifica con la siguiente función:
@State var isEditMode: EditMode = .inactive ... .environment(.editMode, $isEditMode)
De esta forma cuando modifiquemos la variable de estado isEditMode
también se modificará la variable de entorno editMode
.
struct ContentView : View { @State var isEditMode: EditMode = .inactive @State private var items = ["Iron Man", "The Incredible Hulk", "Iron Man 2", "Thor", "Captain America", "Marvel's The Avengers"] var body: some View { NavigationView { List { ForEach(items, id: .self) { Text($0) } .onDelete { items.remove(atOffsets: $0) } } } .navigationBarItems(trailing: Group { if isEditMode == .inactive { Button("Start Edit") { withAnimation { isEditMode = .active } } .padding(.all, 5) .background(Color.green.opacity(0.8)) } else { Button("End Edit") { withAnimation { isEditMode = .inactive } } .padding(.all, 5) .background(Color.red.opacity(0.8)) } } ) .environment(.editMode, $isEditMode) } }
Para que veamos algún cambio visual al modificar el editMode
es necesario que las vistas implementen alguno de los modificadores necesarios que afecta a este modo, como el onDelete
en los ejemplos anteriores.
Operaciones comunes sobre el componente List
Los datos que se muestran en un componente List
suelen ser datos dinámicos que son proporcionados por una base de datos o un web service externos a la aplicación. En muchas ocasiones estos datos que obtenemos son para que el usuario opere con ellos, añadiendo nuevos, seleccionándolos, modificando el orden o eliminándolos. A continuación veremos como realizar las siguientes operaciones que son muy comunes sobre un List
- Navegación
- Seleccionar elementos
- Eliminar elementos
- Mover elementos
Cómo navegar a una nueva pantalla al pulsar sobre una celda de un List
Si venimos de UIKit
seguramente que conozcamos el método -tableView:didSelectRowAtIndexPath:
. A través de este método obteníamos el indice seleccionado y con este dato decidíamos la acción que se debía ejecutar.
En SwiftUI
para conseguir ese mismo efecto debemos implementar el método onTapGesture
para tener un control similar al indicado anteriormente, pero si lo que queremos es navegar a una nueva pantalla la implementación correcta será implementar un NavigationLink
. Aquí puedes consultar más información sobre los componentes de navegación.
Al añadir un NavigationLink
en una celda encontramos un comportamiento especial diferente de cuando lo usamos en otros sitios: le da a la celda un estilo visual único en el que añade una flecha a la derecha indicando que existe navegación.
struct ContentView: View { var body: some View { NavigationView { List { ForEach(0..<30) { NavigationLink("Show: Go to index: ($0)", destination: Text("Index: ($0)")) } } } } }
Este tipo de comportamientos es común en SwiftUI
y se encuentra en otros componentes (como Form
).
Con esta implementación conseguimos que toda la celda se pueda pulsar y ejecutará la acción de navegación.
Hay otras ocasiones donde no queremos que se vea esa flecha que añade el NavigationLink
. Para estos casos necesitamos que el NavigationLink
sea un EmptyView
y lo tendremos que añadir como overlay
a la primera vista de la celda.
struct ContentView: View { var body: some View { NavigationView { List { ForEach(0..<30) { cell($0) } } } } func cell(_ index: Int) -> some View { HStack { Text("Empty: Go to index: (index)") } .overlay( NavigationLink(destination: Text("Index: (index)")) { EmptyView() } ) } }
Con esta implementación también conseguimos que toda la celda se pueda pulsar para ejecutar la acción del NavigationLink
.
Por último, nos podemos encontrar otros casos donde queremos que la navegación sólo se ejecute al pulsar un botón o una imagen y no toda la celda. Para estos casos seguiremos usando un NavigationLink
pero tendremos que realizar un control manual sobre él para controlar correctamente la navegación.
- De forma similar al caso anterior añadiremos un
NavigationLink
con unEmptyView
comooverlay
de la celda, pero es importante que se le añada el modificadorhidden
. - En la inicialización del NavigationLink tendremos que usar los parámetros
tag
yselection
:tag
: Identificador único de la celda. Puede ser un ID o el propio indiceselection
: Variable de estado del mismo tipo que eltag
, pero opcional.
- Añadiremos un
Button
que será quien desencadene la navegación. La acción de esteButton
consistirá en modificar la variable de estadoselection
para setear el valor deltag
al que queremos realizar la navegación.
Todo esto nos deja la siguiente implementación:
struct ContentView: View { @State private var navigateToNext: Int? var body: some View { NavigationView { List { ForEach(0..<30) { cell($0) } } } } func cell(_ index: Int) -> some View { HStack { Button("Hidden: Go to index: (index)") { navigateToNext = index } .buttonStyle(BorderlessButtonStyle()) } .overlay( NavigationLink( destination: Text("Index: (index)"), tag: index, selection: $navigateToNext) { EmptyView() } .hidden() ) } }
A continuación vemos un ejemplo con estos tres casos incluidos:
struct ContentView: View { @State private var navigateToNext: Int? var body: some View { NavigationView { List { ForEach(0..<30) { if $0 < 10 { navigationLinkShow($0) } else if $0 < 20 { navigationLinkEmpty($0) } else { navigationLinkHidden($0) } } } } } func navigationLinkShow(_ index: Int) -> some View { NavigationLink("Show: Go to index: (index)", destination: Text("Index: (index)")) } func navigationLinkEmpty(_ index: Int) -> some View { HStack { Text("Empty: Go to index: (index)") } .overlay( NavigationLink(destination: Text("Index: (index)")) { EmptyView() } ) } func navigationLinkHidden(_ index: Int) -> some View { HStack { Button("Hidden: Go to index: (index)") { navigateToNext = index } .buttonStyle(BorderlessButtonStyle()) } .overlay( NavigationLink( destination: Text("Index: (index)"), tag: index, selection: $navigateToNext) { EmptyView() } .hidden() ) } }
Como permitir la selección de celdas en un List (simple y múltiple)
Para implementar la selección de celdas en un List
tendremos que usar el parámetro selection
que podemos encontrar en su inicializador.
@State var selection: String? ... List(selection: $selection) { ForEach(items, id: .self) { Text($0) } }
La variable selection
deberá ser del mismo tipo que la variable id
del ForEach
encargado de devolver cada celda. Hay otras ocasiones en las que el ForEach
no especifica explícitamente la variable id
. Esto ocurre cuando iteramos sobre un listado que implementa el protocolo Identifiable
, siendo implícitamente su id
el propio id
del ForEach
.
De esta forma el comportamiento que obtendremos será que el valor de la variable selection
cambiará cada vez que se seleccione una nueva celda. Este ejemplo es para la selección de un único elemento del List
. Si queremos seleccionar varios elementos habría que cambiar el tipo de la variable selection
a una colección:
@State var selection = Set()
Ejemplo de selección simple
struct ContentView : View { @State var selection: UUID? @State private var items = Film.mockFilms var body: some View { VStack(spacing: 15) { List(selection: $selection) { ForEach(items) { Text($0.name) } } if let uuid = selection, let film = items.first(where: { $0.id == uuid }) { VStack(spacing: 5) { Text("Your select:") .font(.headline) Text("(film.name)") .font(.body) } .padding() } } .navigationBarItems(trailing: EditButton()) } struct Film: Identifiable { let id: UUID = UUID() var name: String static let mockFilms = [Film(name: "Iron Man"), Film(name: "The Incredible Hulk"), Film(name: "Iron Man 2"), Film(name: "Thor"), Film(name: "Captain America: The First Avenger"), Film(name: "Marvel's The Avengers")] } }
Ejemplo de selección multiple
struct ListSelectionMultipleView : View { @State var selection = Set() @State private var items = Film.mockFilms var body: some View { VStack(spacing: 15) { List(selection: $selection) { ForEach(items) { Text($0.name) } } showSelectedFilms() } .navigationBarItems(trailing: EditButton()) } func showSelectedFilms() -> some View { if !selection.isEmpty { let result = items.compactMap { (film) -> Film? in selection.contains(film.id) ? film : nil }.map { return $0.name } return AnyView( VStack(spacing: 5) { Text("Your select:") .font(.headline) Text("(result.joined(separator: ", "))") .font(.body) } .padding()) } else { return AnyView(EmptyView()) } } struct Film: Identifiable { let id: UUID = UUID() var name: String static let mockFilms = [Film(name: "Iron Man"), Film(name: "The Incredible Hulk"), Film(name: "Iron Man 2"), Film(name: "Thor"), Film(name: "Captain America: The First Avenger"), Film(name: "Marvel's The Avengers")] } }
Cómo eliminar y mover elementos en un List
Para eliminar y mover elementos en un List
se usa una implementación similar. En ambos casos la implementación se realiza sobre el ForEach
encargado de iterar sobre el listado de elementos que queremos mostrar. Este componente nos proporciona los siguientes métodos:
onDelete
. Permite controlar los eventos de borrado sobre una colecciónonMove
. Permite controlar los eventos de reordenación sobre una colección.
La implementación consiste en añadir estos métodos al ForEach
y modificar la colección original (que será una variable de estado o similar) en base a los parámetros recibidos en cada método.
- El método
onDelete
nos proporciona los indices que se deben eliminar de la colección - El método
onMove
nos proporciona los indices originales de las celdas que se deben mover y los indices de donde se deben mover.
En ambos casos la implementación es muy sencilla:
onDelete
@State private var items = Film.mockFilms ... List { ForEach(items) { Text($0.name) } .onDelete { items.remove(atOffsets: $0) } }
onMove
@State private var items = Film.mockFilms ... List { ForEach(items) { Text($0.name) } .onMove { items.move(fromOffsets: $0, toOffset: $1) } }
Ejemplo completo
struct ContentView : View { @State var isEditMode: EditMode = .inactive @State private var items = Film.mockFilms var body: some View { VStack(spacing: 15) { List { ForEach(items) { Text($0.name) } .onDelete { items.remove(atOffsets: $0) } .onMove { items.move(fromOffsets: $0, toOffset: $1) } } Group { if isEditMode == .inactive { Button("Start Edit") { withAnimation { isEditMode = .active } } .padding() .background(Color.green.opacity(0.3)) } else { Button("End Edit") { withAnimation { isEditMode = .inactive } } .padding() .background(Color.red.opacity(0.3)) } } .padding() } .navigationBarItems(trailing: EditButton()) .environment(.editMode, $isEditMode) .navigationBarTitle("Edit", displayMode: .inline) .navigationColor(background: UIColor(red: 31/255, green: 155/255, blue: 222/255, alpha: 1), title: .white) } struct Film: Identifiable { let id = UUID() var name: String static let mockFilms = [Film(name: "Iron Man"), Film(name: "The Incredible Hulk"), Film(name: "Iron Man 2"), Film(name: "Thor"), Film(name: "Captain America"), Film(name: "Marvel's The Avengers")] } }
Ejemplo
Puedes encontrar este ejemplo en https://github.com/SDOSLabs/SwiftUI-Test bajo el apartado List.
Rafael Fernández,
iOS Tech Lider