Aprende a crear una UITableView y UITableViewCell por código
Aprende a crear una UITableView y UITableViewCell por código

UITableView y UITableViewCell en UIKit con Swift en Español - Curso iOS

UITableView sirve para poder mostrar un listado de información. Cada elemento de nuestra UITableView lo representamos con una UITableViewCell. Hoy aprendemos a cómo crear por código una UITableView, con celda custom, diferentes headers, secciones, etc

SwiftBeta

Tabla de contenido

Aprende a crear UITableView y UITableViewCell en Swift con UIKit

Hoy en SwiftBeta vamos a ver las famosas UITableView y UITableViewCell. Vamos a crear una app que muestra un listado de devices. Estos devices estarán separados por secciones dentro del UITableView, y podremos ver todos ellos al hacer scroll. También crearemos una vista custom que representará nuestra celda, y lo haremos paso a paso. Aparte de aprender a crear TableViews, también sacaremos toda responsabilidad de nuestro ViewController para dejar un código más mantenible y escalable, al final nuestro ViewController tendrá muy pocas líneas de código. Si te gustan los videos del canal y quieres apoyarlo, suscríbete. De esta manera seguiré creando contenido cada semana.

Creamos el proyecto en Xcode

Lo primero de todo que vamos hacer es crear el proyecto en Xcode. Acuerdate que estamos usando UIKit, es por eso que debes seleccionar Storyboard en Interface.

Una vez hemos creado el proyecto nos vamos al ViewController. Allí dentro vamos a crear nuestra primera instancia de UITableView.

import UIKit

class ViewController: UIViewController {
    
    private let devicesTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        devicesTableView.backgroundColor = .blue
        view.addSubview(devicesTableView)
        
        NSLayoutConstraint.activate([
            devicesTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            devicesTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            devicesTableView.topAnchor.constraint(equalTo: view.topAnchor),
            devicesTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}
Añadimos una UITableView a nuestro UIViewController

Hemos aprovechado y hemos añadido nuestro UITableView a la View del ViewController. También hemos aplicado constraints para que la TableView ocupe todo el espacio de la vista del ViewController.

Creamos la estructura de datos a mostrar en nuestra UITableView

A continuación, vamos a crear una Struct (podemos hacerlo en un fichero aparte o en el mismo fichero del ViewController) que va a representar la información que vamos a mostrar en cada celda de nuestro UITableView. La struct la vamos a llamar Device:

struct Device {
    let title: String
    let imageName: String
}
Creamos Struct Device

Ahora que ya tenemos la struct, vamos a crear un array con información:

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]
Array con los datos que representaremos en nuestra UITableView

La constante house contiene un array que va a representar información de los devices que tengo en casa. Cada elemento de este Array será una celda en nuestro UITableView, ¿pero cómo hacemos para conectar los datos con la vista? Para hacerlo necesitamos usar la propiedad datasource de nuestro UITableView.

Conformamos el protocolo UITableViewDataSource

Esta propiedad espera que el objecto, la instancia que le asignemos aquí, conforme el protocolo UITableViewDataSource. Y esta instancia, sabe como proporcionar los datos a la vista, ya que debe implementar unos métodos que son obligatorios. De momento, asignamos a la propiedad dataSource de nuestro UITableView el valor de self. Y por lo tanto deberemos conformar el protocolo UIViewTableDataSource en nuestro ViewController. Esto será temporal, ya que luego lo moveremos a otra clase para que nos quede un código mucho más limpio.

Es decir, deberíamos tener el siguiente código implementado:

struct Device {
    let title: String
    let imageName: String
}

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]

class ViewController: UIViewController, UITableViewDataSource {
    
    private let devicesTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        devicesTableView.backgroundColor = .blue
        devicesTableView.dataSource = self
        view.addSubview(devicesTableView)
        
        NSLayoutConstraint.activate([
            devicesTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            devicesTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            devicesTableView.topAnchor.constraint(equalTo: view.topAnchor),
            devicesTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}
El siguiente paso es conectar los datos con nuestra UITableView

Vemos que aparece un error, esto es correcto ya que hemos añadido UITableViewDataSource en nuestro ViewController (justo después de heredar de UIViewController), pero no lo estamos conformando todavía, falta utilizar los método mínimos para que se deje de quejar.
Vamos a pulsar el button de Xcode para que nos autocomplete y desparezca el error:

Error al intentar conformar UITableViewDataSource en nuestro ViewController
Error al intentar conformar UITableViewDataSource en nuestro ViewController

Y al hacerlo, aparece en nuestro código dos método para ser rellenados:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        <#code#>
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        <#code#>
    }
Métodos a implementar de nuestro UITableViewDataSource
  • El primer método espera el número de elementos que queremos mostrar en nuestro UITableView
  • El segundo método espera cómo representar visualmente la información en una celda de nuestro UITableView

Vamos a rellenar estos dos métodos, pero ¿por qué solo estos dos? estos son lo obligatorios que debemos rellenar si conformamos el protocolo UITableViewDataSource. Si navegamos hasta este protocolo podemos ver todos los métodos que podríamos implementar en nuestra clase ViewController.
Spoiler: veremos algunos de estos más adelante en el video.

Rellenamos método numberOfRowsInSection

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
Primer método que debemos rellenar

Debemos dar un valor del número de elementos que queremos mostrar en nuestra UITableView. Este es muy sencillo, bastaría con poner el número de elementos de nuestro Array:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        house.count
    }
Retornamos el número de elementos de nuestro Array house

Rellenamos método cellForRowAt indexPath

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
Segundo método que debemos rellenar

Aquí, lo que debemos hacer es preguntar a nuestro UITableView qué mostrar por cada elemento de nuestra constante house (el Array de devices que hemos creado hace uno momento). Y para ello nuestro UITableView nos retorna una celda vacía y la tenemos que rellenar con la información que queremos (como si fuera un cascaron). Cuando mostramos celdas en un UITableView, se maneja muy bien la memoria, imagina que tienes 1000 elementos que quieres mostrar, al hacer scroll, las celdas que van despareciendo se van liberando y de esta manera vamos liberando memoria de nuestro UITableView. Vamos a escribir el siguiente código:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "", for: indexPath)
        return cell
    }
Creamos una instancia de UITableViewCell y la retornamos

Si probamos de compilar, vamos a obtener un crash de nuestra aplicación.

Al compilar obtenemos un error, vamos a ver por qué
Al compilar obtenemos un error, vamos a ver por qué

Este crash nos está ocurriendo por que a nuestra UITableView le falta información. Le falta saber qué celdas tenemos registradas en nuestro UITableView. Y si te fijas este paso no lo hemos hecho aún. Vamos hacer dos cambios:

Lo primero de todo es registrar qué tipo de celdas se pueden usar en nuestro UITableView, para hacerlo vamos a registrar una del sistema dentro del método viewDidLoad, con la siguiente línea:

        devicesTableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
Registramos la celda en nuestra UITableView

A continuación, vamos a moficiar el método cellForRowAt indexPath y le vamos a añadir el identificador de celda, en nuestro caso hemos usado UITableViewCell, pero podríamos utilizar cualquier otro nombre:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        return cell
    }
Compilamos y vemos el resultado en el simulador

Si ahora compilamos, obtenemos como resultado unas celdas vacías. Pero! fíjate que son 5 celdas vacías, las mismas que tenemos en la constante house, ahora solo faltaría rellenarla con datos:

Simulador mostrando las 5 celdas equivalentes a los 5 elementos de nuestro Array house
Simulador mostrando las 5 celdas equivalentes a los 5 elementos de nuestro Array house

Rellenamos información de la celda UITableViewCell

Para poder rellenar la celda, necesitamos sabe qué modelo le toca a cada celda. Y también debemos crear una instancia de UIListContentConfiguration.cell() Nos quedaría un código como el siguiente:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath)
        
        let model = house[indexPath.row]
        
        var listContentConfiguration = UIListContentConfiguration.cell()
        listContentConfiguration.text = model.title
        listContentConfiguration.image = UIImage(systemName: model.imageName)
        
        cell.contentConfiguration = listContentConfiguration
        return cell
    }
Configuramos nuestra UITableView cell con UIListContentConfiguration

Si compilamos vamos a ver qué ocurre:

Simulador mostrando la información en cada celda
Simulador mostrando la información en cada celda

Ahora mismo podríamos quitar el backgroundColor de nuestro UITableView, el propósito es que se viera más claro cuántas celdas se mostraban.

Aquí hay varios puntos que me gustaría destacar, para poder mostrar la información en nuestra UITableView hemos usado UITableViewCell, pero podríamos crear nuestra propia vista para hacerlo.Vamos a ver cómo lo haríamos.

Creamos una custom UITableViewCell

Pulsamos CMD+N y creamos un fichero nuevo, en el template escogemos Cocoa Touch Class

Creamos una Cocoa Touch Class
Creamos una Cocoa Touch Class

Y le voy a poner el nombre de SwiftBetaCustomCell

Seleccionamos como subclase UITableViewCell
Seleccionamos como subclase UITableViewCell

Y vamos a añadir el siguiente código:

import UIKit

class SwiftBetaCustomCell: UITableViewCell {
    private let deviceImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let deviceNameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 24)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        addSubview(deviceImageView)
        addSubview(deviceNameLabel)
        
        NSLayoutConstraint.activate([
            deviceImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            deviceImageView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
            deviceImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
            deviceImageView.widthAnchor.constraint(equalToConstant: 40),
            deviceImageView.heightAnchor.constraint(equalToConstant: 40),
            
            deviceNameLabel.leadingAnchor.constraint(equalTo: deviceImageView.trailingAnchor, constant: 20),
            deviceNameLabel.centerYAnchor.constraint(equalTo: deviceImageView.centerYAnchor),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(model: Device) {
        deviceImageView.image = UIImage(systemName: model.imageName)
        deviceNameLabel.text = model.title
    }
}
Creamos nuestra propia celda UITableViewCell

Muy importante el último método, nos serviará para poder configurar la celda. Lo siguiente que vamos hacer, es registrar esta nueva celda en UITableView, tal y como hemos hecho hace un momento con UITableViewCell:

devicesTableView.register(SwiftBetaCustomCell.self, forCellReuseIdentifier: "SwiftBetaCustomCell")
Registramos nuestra nueva celda en la TableView

Por último, modificamos el método cellForRowAt indexPath, ahora usaremos la nueva celda que acabamos de crear:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SwiftBetaCustomCell", for: indexPath) as! SwiftBetaCustomCell
        
        let model = house[indexPath.row]
        cell.configure(model: model)
        
        return cell
    }
Configuramos nuestra nueva celda en nuestra UITableView y le pasamos la información del modelo

Conformamos protocolo UITableViewDelegate

Vamos a seguir viendo más ventajas de UITableView, podemos conformar el protocolo UITableViewDelegate para detectar eventos que pasan en nuestra UITableView. Como por ejemplo:

  • Cuando se pulsa una celda, esto sería muy útil para lanzar acciones. Imagina que tocamos una celda y queremos navegar a una pantalla nueva.
  • Podemos saber si se realiza alguna swipeActions en una celda
  • también el tamaño de cada celda. En nuestro ejemplo todas las celdas tienen el mismo tamaño, pero puede haber casos en que cada celda tenga un tamaño dinámico.
  • etc

Vamos a conformar el protocolo UITableViewDelegate para poder detectar cuando una celda es pulsada.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    private let devicesTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        devicesTableView.dataSource = self
        devicesTableView.delegate = self
Conformamos el protocolo UITableViewDelegate en nuestra UITableView

En este caso, si compilamos no obtenemos ningún error, ya que los métodos de UITableViewDelegate son opcionales. Vamos a implementar el método para saber que una celda se ha pulsado:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let model = house[indexPath.row]
        print("CELL: \(model.title)")
    }
Cada vez que se pulse una celda se printará un mensaje por consola

Si compilamos, cuando seleccionamos una celda se printa por consola la información de la celda.

Limpiamos código, dividimos responsabilidades

Extraemos UITableViewDataSource a una nueva clase

Nuestro ViewController está empezando a tener demasiada responsabilidad, ahora lo que vamos hacer es separar responsabilidades, vamos a intentar dejar nuestro ViewController lo más limpio posible. Pulsamos CMD+N y creamos un nuevo fichero, lo vamos a llamar SwiftBetaTableViewDataSource

import Foundation
import UIKit

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]

final class SwiftBetaTableViewDataSource: NSObject, UITableViewDataSource {
    private let dataSource: [Device]
    
    init(dataSource: [Device]) {
        self.dataSource = dataSource
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        dataSource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SwiftBetaCustomCell", for: indexPath) as! SwiftBetaCustomCell
        
        let model = dataSource[indexPath.row]
        cell.configure(model: model)
        
        return cell
    }
}
Creamos SwiftBetaTableViewDataSource con el código de UITableViewDataSource

SwiftBetaTableViewDataSource espera un array de modelos, estos los tenemos hardcodeados dentro de la app, pero podrían venir de alguna petición HTTP a nuestro servidor. Para hacer el video más simple, esta información la tenemos en una constante.

Extraemos UITableViewDelegate a una nueva clase

Lo siguiente que vamos hacer es extraer el UITableViewDelegate en un fichero aparte, y lo vamos a llamar SwiftBetaTableViewDataSource

import Foundation
import UIKit

final class SwiftBetaTableViewDelegate: NSObject, UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let model = house[indexPath.row]
        print("CELL: \(model.title)")
    }
}
Creamos SwiftBetaTableViewDelegate con el código de UITableViewDelegate

Y al sacar toda esta responsabilidad de nuestro ViewController quedaría de la siguiente manera:

class ViewController: UIViewController {
    private let devicesTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()
    
    private var dataSource: SwiftBetaTableViewDataSource?
    private var delegate: SwiftBetaTableViewDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.dataSource = SwiftBetaTableViewDataSource(dataSource: house)
        self.delegate = SwiftBetaTableViewDelegate()
        devicesTableView.dataSource = dataSource
        devicesTableView.delegate = delegate
        devicesTableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
        devicesTableView.register(SwiftBetaCustomCell.self, forCellReuseIdentifier: "SwiftBetaCustomCell")
        view.addSubview(devicesTableView)
        
        NSLayoutConstraint.activate([
            devicesTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            devicesTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            devicesTableView.topAnchor.constraint(equalTo: view.topAnchor),
            devicesTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}
Usamos las nuevas clases en nuestro UIViewController

Limpiamos nuestro ViewController

Una vez hemos sacado todas esas responsabilidades, podemos limpiar la creación de la vista en nuestro UIViewController:

class ViewController: UIViewController {
    private var dataSource: SwiftBetaTableViewDataSource?
    private var delegate: SwiftBetaTableViewDelegate?
    
    override func loadView() {
        let tableView = UITableView()
        self.dataSource = SwiftBetaTableViewDataSource(dataSource: house)
        self.delegate = SwiftBetaTableViewDelegate()
        tableView.dataSource = dataSource
        tableView.delegate = delegate
        tableView.register(SwiftBetaCustomCell.self, forCellReuseIdentifier: "SwiftBetaCustomCell")

        view = tableView
    }
}
Simplificamos la creación de la UITableView en nuestro ViewController

Bastante limpio! hemos borrado muchas líneas de código 👏

Vamos a ver qué más podemos hacer con las UITableViews

Añadir Headers

Podemos añadir headers y footers, a continuación vamos a ver el ejemplo de un header, para añadir el siguiente código debemos ir a la clase SwiftBetaTableViewDataSource:

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "Devices House"
    }
Añadimos un Header en nuestra UITableView

Si compilamos vemos el siguiente header

Mostramos un título en el header de nuestra UITableView
Mostramos un título en el header de nuestra UITableView

vamos a añadir más secciones a nuestra UITableView. Para hacerlo necesitamos:

  • Más datos, un Array nuevo
  • Crear al método numberOfSections
  • Modificar todos los sitios donde extramos el valor del model. Ahora primero debemos especificar la sección para saber cuántos elementos tiene.
  • Diferenciar título entre secciones
import Foundation
import UIKit

let house = [
    Device(title: "Laptop", imageName: "laptopcomputer"),
    Device(title: "Mac mini", imageName: "macmini"),
    Device(title: "Mac Pro", imageName: "macpro.gen3"),
    Device(title: "Pantallas", imageName: "display.2"),
    Device(title: "Apple TV", imageName: "appletv")
]

let work = [
    Device(title: "iPhone", imageName: "iphone"),
    Device(title: "iPad", imageName: "ipad"),
    Device(title: "Airpods", imageName: "airpods"),
    Device(title: "Apple Watch", imageName: "applewatch")
]

let allMyDevices = [house, work]

final class SwiftBetaTableViewDataSource: NSObject, UITableViewDataSource {
    private let dataSource: [[Device]]
    
    init(dataSource: [[Device]]) {
        self.dataSource = dataSource
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        dataSource.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        dataSource[section].count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "SwiftBetaCustomCell", for: indexPath) as! SwiftBetaCustomCell
        
        let model = dataSource[indexPath.section][indexPath.row]
        cell.configure(model: model)
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if section == 0 {
            return "Device House"
        } else {
            return "Device Work"
        }
    }
}
Modificamos el código para que funcione con diferentes secciones nuestra UITableView

Si compilamos, podemos ver

Simulador muestra diferentes secciones en nuestra UITableView
Simulador muestra diferentes secciones en nuestra UITableView

Aquí, podríamos hacer como lo que hemos hecho con la celda, crear una Custom, añadir las subvistas que quereamos, añadir un height más grande, etc

Conclusión

Hoy hemos aprendido a cómo crear una UITableView. Al principio hemos creado una celda que nos proporciona UIKit llamada UITableViewCell. Y luego hemos creado una custom, una en la que hemos podido añadir las subvistas que hemos querido. Lo siguiente que hemos hecho ha sido limpiar nuestro ViewController, hemos sacado toda responsabilidad a otras clases, y finalmente hemos creado un header para cada sección de nuestros datos de la TableView. Se podrían hacer muchas más cosas, pero como video introductorio lo dejaremos aquí.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple