
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
Tabla de contenido

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),
])
}
}
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
}
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")
]
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),
])
}
}
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:

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#>
}
- 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
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
}
Rellenamos método cellForRowAt indexPath
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
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
}
Si probamos de compilar, vamos a obtener un crash de nuestra aplicación.

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")
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
}
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:

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
}
Si compilamos vamos a ver qué ocurre:

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

Y le voy a poner el nombre de SwiftBetaCustomCell

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
}
}
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")
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
}
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
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)")
}
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
}
}
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)")
}
}
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),
])
}
}
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
}
}
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"
}
Si compilamos vemos el siguiente header

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"
}
}
}
Si compilamos, podemos ver

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í.