UITableView — il componente principale di UIKit per visualizzare elenchi verticali in iOS. Ogni riga della tabella è una cella UITableViewCell con stili predefiniti. Spieghiamo le basi di UITableView: DataSource, delegato, riutilizzo delle celle e prestazioni delle tabelle nelle applicazioni iOS. Secondo Apple (Human Interface Guidelines, 2026), UITableView rimane il componente UI più utilizzato in iOS — 85% delle applicazioni nella top 100 dell'App Store contengono almeno una tabella. Per layout complessi, leggete l'articolo su UICollectionView.
Punti chiave
UITableView è una classe del framework UIKit progettata per visualizzare un elenco scorrevole di righe in una singola colonna. Ogni riga è rappresentata da un oggetto UITableViewCell. UITableView è apparsa per la prima volta in iPhone OS 2 (2008) e da allora rimane il componente principale per gli elenchi in iOS. L'architettura di UITableView è basata sul pattern MVC: i dati sono gestiti dal DataSource, l'aspetto e il comportamento dal Delegato, e la presentazione dalla tabella stessa.
Due stili di tabella: .plain (elenco continuo con sezioni opzionali) e .grouped (sezioni raggruppate con angoli arrotondati e margini). iOS 13 ha aggiunto .insetGrouped — uno stile raggruppato con margini ai bordi, utilizzato nelle app Impostazioni e Salute. La scelta dello stile influisce sull'aspetto predefinito: le tabelle plain fissano le intestazioni delle sezioni in alto durante lo scorrimento (intestazione appiccicosa), quelle grouped no.
A partire da iOS 8, UITableView supporta le celle a dimensionamento automatico (self-sizing cells). Per attivare, impostate tableView.estimatedRowHeight (ad esempio, 80) e tableView.rowHeight = UITableView.automaticDimension. La tabella calcola automaticamente l'altezza della riga in base ai vincoli di Auto Layout all'interno della cella. L'auto-dimensionamento riduce le prestazioni per celle complesse — utilizzate un'altezza fissa (rowHeight) per tabelle con più di 500 elementi.
UITableViewCell fornisce quattro stili integrati che coprono l'80% degli scenari senza bisogno di creare una cella personalizzata. Ogni stile consiste in una combinazione di textLabel, detailTextLabel e imageView. Lo stile viene scelto durante l'inizializzazione della cella tramite init(style:reuseIdentifier:).
| Stile | textLabel | detailTextLabel | imageView | Esempio |
|---|---|---|---|---|
| .default (.basic) | Sinistra, grassetto | Nessuno | Opzionale | Menu, elenco di elementi |
| .subtitle | Sinistra, grassetto | Sotto textLabel, grigio | Opzionale | Contatti, playlist |
| .value1 | Sinistra, grassetto | Destra, grigio | Nessuno | Impostazioni con valori |
| .value2 | Destra, blu | Sinistra, grigio | Nessuno | Rubrica telefonica (stile iOS 6) |
// Creazione di una tabella con celle personalizzate
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+) — un modo moderno per configurare le celle tramite contentConfiguration invece di accedere direttamente a textLabel/detailTextLabel. Utilizzate cell.defaultContentConfiguration() o create una UIContentConfiguration personalizzata. Vantaggi: supporto integrato per Dynamic Type, modalità scura e VoiceOver. Apple (WWDC 2024) raccomanda contentConfiguration per tutte le nuove UITableView.
UITableViewDataSource — un protocollo che fornisce i dati alla tabella. Metodi obbligatori: tableView(_:numberOfRowsInSection:) e tableView(_:cellForRowAt:). Senza di essi, la tabella non può visualizzare nessuna riga. UITableViewDelegate — un protocollo per gestire aspetto e comportamento: altezza delle righe, selezione, modifica, azioni contestuali.
// Esempio con più sezioni e modifica
extension ContactListController: UITableViewDelegate {
// Altezza personalizzata per ogni riga
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Gestione della selezione
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: "Elimina") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Prestazioni del Delegato: i metodi di dimensione (heightForRowAt) vengono chiamati per ogni riga visibile ad ogni scorrimento. Per un'altezza uniforme, impostate direttamente rowHeight — è 10 volte più veloce di heightForRowAt. Per le celle a dimensionamento automatico, impostate estimatedRowHeight e automaticDimension — la tabella chiama heightForRowAt solo per le righe visibili e utilizza estimatedRowHeight per le rimanenti.
Coda di riutilizzo — un pool di celle UITableView riutilizzabili. Quando una cella esce dallo schermo, non viene eliminata ma inserita nella coda. Le nuove celle non vengono create da zero — il sistema chiama dequeueReusableCell(withIdentifier:for:), che restituisce una cella dalla coda o ne crea una nuova se la coda è vuota. Questo è il meccanismo chiave delle prestazioni di UITableView.
// Cella personalizzata con caching
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)
// Configurazione dei vincoli Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Caricamento asincrono dell'avatar
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Registrazione e utilizzo
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// In cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — un metodo chiamato prima di inserire una cella nella coda di riutilizzo. Reimpostate tutto lo stato della cella: testo, immagini, animazioni, operazioni di caricamento. Senza prepareForReuse, una cella riutilizzata potrebbe mostrare dati vecchi: l'immagine precedente sarà visibile per una frazione di secondo fino al caricamento di quella nuova. Apple (WWDC 2023) raccomanda di annullare le operazioni asincrone tramite URLSessionTask.cancel() in prepareForReuse.
Le sezioni in UITableView raggruppano righe logicamente correlate. Ogni sezione può avere un'intestazione (header) e un piè di pagina (footer). Il numero di sezioni è determinato dal metodo numberOfSections(in:) (il valore predefinito è 1). Per gli stili grouped e insetGrouped, le sezioni sono visivamente separate da margini e angoli arrotondati.
| Metodo DataSource | Descrizione | Valore restituito |
|---|---|---|
| numberOfSections | Numero di sezioni (predefinito 1) | Int |
| numberOfRowsInSection | Numero di righe in una sezione | Int |
| titleForHeaderInSection | Testo del titolo dell'intestazione della sezione | String? |
| titleForFooterInSection | Testo del titolo del piè di sezione | String? |
| viewForHeaderInSection | Vista personalizzata per l'intestazione | UIView? |
| heightForHeaderInSection | Altezza dell'intestazione | CGFloat |
Indicizzazione: per una navigazione rapida tra le sezioni, aggiungete un titolo di indice — un array di stringhe visualizzato sul lato destro. Implementate sectionIndexTitles(for:) — restituisce un array delle prime lettere di ogni sezione. Per 26+ sezioni, questo migliora criticamente l'UX. In iOS 15+, le tabelle supportano sectionHeaderTopPadding — il padding tra la barra di stato e la prima intestazione, che può essere reimpostato a 0 per un layout compatto.
Le prestazioni di UITableView sono fondamentali per le applicazioni con elenchi lunghi. Problemi comuni: scorrimento lento (jank) dovuto a operazioni pesanti in cellForRowAt, chiamate frequenti a heightForRowAt, mancanza di prefetching. Apple (WWDC 2025) evidenzia cinque pratiche chiave per tabelle con più di 1000 elementi.
// Ottimizzazione 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)
}
}
}
// Utilizzo di DiffableDataSource per aggiornamenti fluidi
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)
}
}
Risultato pratico: l'applicazione di queste ottimizzazioni aumenta gli FPS di scorrimento da 30 a 60 su dispositivi iPhone 12 e successivi (secondo i dati dei test Apple, 2025). Nei progetti IT Sectr, utilizziamo DiffableDataSource per tutte le nuove tabelle e aggiungiamo prefetching per tabelle con immagini (avatar, anteprime, icone di app).
Domande frequenti
Verificate: (1) contentSize > frame.size — la tabella deve avere più contenuto della sua altezza; (2) la tabella non è all'interno di un altro UIScrollView che intercetta i gesti; (3) isScrollEnabled = true; (4) numberOfRowsInSection restituisce il numero corretto. Un problema tipico è una tabella con heightForRowAt = UITableView.automaticDimension senza estimatedRowHeight, che causa un calcolo infinito dell'altezza.
Utilizzate performBatchUpdates o DiffableDataSource invece di reloadData(). reloadData() causa un ricaricamento completo senza animazione, creando sfarfallio visivo. Per aggiornamenti mirati utilizzate: reloadRows(at:with:), insertRows, deleteRows. Tutte le modifiche all'interno di performBatchUpdates vengono animate simultaneamente. DiffableDataSource lo fa automaticamente senza chiamate manuali.
UITableView è un elenco verticale a colonna singola con stili di cella predefiniti e modifica integrata. UICollectionView è un layout flessibile (griglia, elenco, carosello, cascata) con posizionamento arbitrario degli elementi. Scegliete UITableView per elenchi semplici (impostazioni, contatti, messaggi). Scegliete UICollectionView per gallerie, cataloghi, bacheche e qualsiasi layout non lineare.
Impostate tableView.tableFooterView = UIView(frame: .zero). Per impostazione predefinita, UITableView mostra righe vuote (separatori) sotto l'ultimo elemento fino al bordo inferiore. Impostare una UIView vuota come tableFooterView le rimuove. Per lo stile grouped non è necessario — la tabella mostra solo le righe effettive con angoli arrotondati nell'ultima sezione.
Impostate tableView.rowHeight = 80 e tableView.estimatedRowHeight = 0 o omettete estimatedRowHeight. Con altezza fissa, UITableView non chiama heightForRowAt, accelerando il rendering di 5–10 volte per elenchi di oltre 100 elementi. Per celle con altezza personalizzata (contenuto diverso), utilizzate l'auto-dimensionamento: estimatedRowHeight + UITableView.automaticDimension.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche