UITableView — el componente principal de UIKit para mostrar listas verticales en iOS. Cada fila de la tabla es una celda UITableViewCell con estilos predefinidos. Explicamos los fundamentos de UITableView: DataSource, delegado, reutilización de celdas y rendimiento de tablas en aplicaciones iOS. Según Apple (Human Interface Guidelines, 2026), UITableView sigue siendo el componente de UI más utilizado en iOS — 85% de las aplicaciones del top 100 de App Store contienen al menos una tabla. Para diseños complejos, lea el artículo sobre UICollectionView.
Puntos clave
UITableView es una clase del framework UIKit diseñada para mostrar una lista desplazable de filas en una sola columna. Cada fila está representada por un objeto UITableViewCell. UITableView apareció por primera vez en iPhone OS 2 (2008) y desde entonces sigue siendo el componente principal para listas en iOS. La arquitectura de UITableView se basa en el patrón MVC: los datos son gestionados por el DataSource, la apariencia y el comportamiento por el Delegate, y la presentación por la propia tabla.
Dos estilos de tabla: .plain (lista continua con secciones opcionales) y .grouped (secciones agrupadas con esquinas redondeadas y márgenes). iOS 13 añadió .insetGrouped — un estilo agrupado con márgenes en los bordes, utilizado en las aplicaciones Ajustes y Salud. La elección del estilo afecta la apariencia predeterminada: las tablas plain fijan los encabezados de sección en la parte superior al hacer scroll (sticky header), las grouped no.
A partir de iOS 8, UITableView admite celdas de tamaño automático (self-sizing cells). Para activarlo, configure tableView.estimatedRowHeight (por ejemplo, 80) y tableView.rowHeight = UITableView.automaticDimension. La tabla calcula automáticamente la altura de la fila basándose en las restricciones de Auto Layout dentro de la celda. El auto-dimensionado reduce el rendimiento para celdas complejas — use altura fija (rowHeight) para tablas con más de 500 elementos.
UITableViewCell proporciona cuatro estilos integrados que cubren el 80% de los escenarios sin necesidad de crear una celda personalizada. Cada estilo consiste en una combinación de textLabel, detailTextLabel e imageView. El estilo se elige al inicializar la celda mediante init(style:reuseIdentifier:).
| Estilo | textLabel | detailTextLabel | imageView | Ejemplo |
|---|---|---|---|---|
| .default (.basic) | Izquierda, negrita | Ninguno | Opcional | Menú, lista de elementos |
| .subtitle | Izquierda, negrita | Debajo de textLabel, gris | Opcional | Contactos, listas de reproducción |
| .value1 | Izquierda, negrita | Derecha, gris | Ninguno | Ajustes con valores |
| .value2 | Derecha, azul | Izquierda, gris | Ninguno | Agenda telefónica (estilo iOS 6) |
// Creación de una tabla con celdas personalizadas
class ContactListController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 60
view.addSubview(tableView)
// Auto Layout
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension ContactListController: UITableViewDataSource {
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
contacts.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let contact = contacts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = contact.name
content.secondaryText = contact.phone
content.image = UIImage(systemName: "person.circle")
cell.contentConfiguration = content
return cell
}
}
UIContentConfiguration (iOS 14+) — una forma moderna de configurar celdas mediante contentConfiguration en lugar de acceder directamente a textLabel/detailTextLabel. Use cell.defaultContentConfiguration() o cree una UIContentConfiguration personalizada. Ventajas: soporte integrado para Dynamic Type, modo oscuro y VoiceOver. Apple (WWDC 2024) recomienda contentConfiguration para todos los UITableView nuevos.
UITableViewDataSource — un protocolo que proporciona datos para la tabla. Métodos obligatorios: tableView(_:numberOfRowsInSection:) y tableView(_:cellForRowAt:). Sin ellos, la tabla no puede mostrar ninguna fila. UITableViewDelegate — un protocolo para gestionar apariencia y comportamiento: altura de filas, selección, edición, acciones contextuales.
// Ejemplo con múltiples secciones y edición
extension ContactListController: UITableViewDelegate {
// Altura personalizada para cada fila
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Manejo de selección
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Swipe-to-delete
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Eliminar") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Rendimiento del Delegate: los métodos de tamaño (heightForRowAt) se llaman para cada fila visible en cada desplazamiento. Para altura uniforme, establezca rowHeight directamente — esto es 10 veces más rápido que heightForRowAt. Para celdas de tamaño automático, configure estimatedRowHeight y automaticDimension — la tabla llama a heightForRowAt solo para las filas visibles y usa estimatedRowHeight para el resto.
Cola de reutilización — un grupo de celdas UITableView reutilizables. Cuando una celda sale de la pantalla, no se elimina sino que se coloca en la cola. Las nuevas celdas no se crean desde cero — el sistema llama a dequeueReusableCell(withIdentifier:for:), que devuelve una celda de la cola o crea una nueva si la cola está vacía. Este es el mecanismo clave de rendimiento de UITableView.
// Celda personalizada con caché
class ContactCell: UITableViewCell {
private let avatarView = UIImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
override init(style: UITableViewCell.Style, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(avatarView)
contentView.addSubview(nameLabel)
contentView.addSubview(subtitleLabel)
// Configuración de restricciones de Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Carga asíncrona de avatar
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Registro y uso
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// En cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — un método llamado antes de colocar una celda en la cola de reutilización. Restablezca todo el estado de la celda: texto, imágenes, animaciones, operaciones de carga. Sin prepareForReuse, una celda reutilizada puede mostrar datos antiguos: la imagen anterior será visible durante una fracción de segundo hasta que se cargue la nueva. Apple (WWDC 2023) recomienda cancelar operaciones asíncronas mediante URLSessionTask.cancel() en prepareForReuse.
Secciones en UITableView agrupan filas lógicamente relacionadas. Cada sección puede tener un header (encabezado) y un footer (pie). El número de secciones se determina mediante el método numberOfSections(in:) (el valor predeterminado es 1). Para los estilos grouped e insetGrouped, las secciones están visualmente separadas por márgenes y esquinas redondeadas.
| Método de DataSource | Descripción | Valor de retorno |
|---|---|---|
| numberOfSections | Número de secciones (predeterminado 1) | Int |
| numberOfRowsInSection | Número de filas en una sección | Int |
| titleForHeaderInSection | Texto del título del encabezado de sección | String? |
| titleForFooterInSection | Texto del título del pie de sección | String? |
| viewForHeaderInSection | Vista personalizada para el encabezado | UIView? |
| heightForHeaderInSection | Altura del encabezado | CGFloat |
Indexación: para una navegación rápida entre secciones, añada un título de índice — un array de cadenas mostradas en el lado derecho. Implemente sectionIndexTitles(for:) — devuelve un array con las primeras letras de cada sección. Para 26+ secciones, esto mejora críticamente la experiencia de usuario. En iOS 15+, las tablas admiten sectionHeaderTopPadding — el padding entre la barra de estado y el primer encabezado, que se puede restablecer a 0 para un diseño compacto.
El rendimiento de UITableView es crítico para aplicaciones con listas largas. Problemas comunes: desplazamiento lento (jank) debido a operaciones pesadas en cellForRowAt, llamadas frecuentes a heightForRowAt, falta de prefetching. Apple (WWDC 2025) destaca cinco prácticas clave para tablas con más de 1000 elementos.
// Optimización con prefetching
extension ContactListController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.prefetch(url: contact.avatarUrl)
}
}
func tableView(_ tableView: UITableView,
cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
let contact = contacts[indexPath.row]
ImageCache.shared.cancelPrefetch(url: contact.avatarUrl)
}
}
}
// Uso de DiffableDataSource para actualizaciones fluidas
class ModernTableController: UIViewController {
private var dataSource: UITableViewDiffableDataSource<Section, Contact>!
func applyContacts(_ contacts: [Contact]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Contact>()
snapshot.appendSections([.main])
snapshot.appendItems(contacts)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Resultado práctico: la aplicación de estas optimizaciones aumenta los FPS de desplazamiento de 30 a 60 en dispositivos iPhone 12 y posteriores (según datos de pruebas de Apple, 2025). En los proyectos de IT Sectr, usamos DiffableDataSource para todas las tablas nuevas y añadimos prefetching para tablas con imágenes (avatares, vistas previas, iconos de aplicaciones).
Preguntas frecuentes
Verifique: (1) contentSize > frame.size — la tabla debe tener más contenido que su altura; (2) la tabla no está dentro de otro UIScrollView que intercepte gestos; (3) isScrollEnabled = true; (4) numberOfRowsInSection devuelve el número correcto. Un problema típico es una tabla con heightForRowAt = UITableView.automaticDimension sin estimatedRowHeight, lo que provoca un cálculo infinito de altura.
Use performBatchUpdates o DiffableDataSource en lugar de reloadData(). reloadData() provoca una recarga completa sin animación, creando un parpadeo visual. Para actualizaciones puntuales use: reloadRows(at:with:), insertRows, deleteRows. Todos los cambios dentro de performBatchUpdates se animan simultáneamente. DiffableDataSource hace esto automáticamente sin llamadas manuales.
UITableView es una lista vertical de una sola columna con estilos de celda predefinidos y edición integrada. UICollectionView es un diseño flexible (cuadrícula, lista, carrusel, cascada) con posicionamiento arbitrario de elementos. Elija UITableView para listas simples (ajustes, contactos, mensajes). Elija UICollectionView para galerías, catálogos, tableros y cualquier diseño no lineal.
Establezca tableView.tableFooterView = UIView(frame: .zero). Por defecto, UITableView muestra filas vacías (separadores) debajo del último elemento hasta el borde inferior. Establecer un UIView vacío como tableFooterView las elimina. Para el estilo grouped esto no es necesario — la tabla solo muestra las filas reales con esquinas redondeadas en la última sección.
Establezca tableView.rowHeight = 80 y tableView.estimatedRowHeight = 0 u omita estimatedRowHeight. Con altura fija, UITableView no llama a heightForRowAt, lo que acelera el renderizado entre 5 y 10 veces para listas de más de 100 elementos. Para celdas con altura personalizada (contenido diferente), use auto-dimensionado: estimatedRowHeight + UITableView.automaticDimension.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también