UITableView — componenta principală UIKit pentru afișarea listelor verticale în iOS. Fiecare rând al tabelului este o celulă UITableViewCell cu stiluri predefinite. Explicăm bazele UITableView: DataSource, delegat, reutilizarea celulelor și performanța tabelelor în aplicațiile iOS. Potrivit Apple (Human Interface Guidelines, 2026), UITableView rămâne cea mai utilizată componentă UI în iOS — 85% dintre aplicațiile din top-100 App Store conțin cel puțin un tabel. Pentru aspecte complexe citiți articolul despre UICollectionView.
Principalele puncte
UITableView — este o clasă din frameworkul UIKit destinată afișării unei liste derulabile de rânduri într-o singură coloană. Fiecare rând este reprezentat de un obiect UITableViewCell. UITableView a apărut în iPhone OS 2 (2008) și de atunci rămâne componenta principală pentru liste în iOS. Arhitectura UITableView este construită pe modelul MVC: datele sunt gestionate de DataSource, aspectul și comportamentul de Delegate, iar vizualizarea de tabelul însuși.
Două stiluri de tabel: .plain (listă continuă cu secțiuni opționale) și .grouped (secțiuni grupate cu colțuri rotunjite și margini). Din iOS 13 a fost adăugat .insetGrouped — stil grouped cu margini de la margini, utilizat în aplicațiile Setări și Sănătate. Alegerea stilului influențează aspectul implicit: tabelele plain fixează headerul secțiunii în partea de sus la derulare (sticky header), grouped — nu.
Începând cu iOS 8, UITableView suportă celule cu dimensiune automată (self-sizing cells). Pentru activare setați tableView.estimatedRowHeight (de exemplu, 80) și tableView.rowHeight = UITableView.automaticDimension. Tabelul calculează automat înălțimea rândului pe baza constrângerilor Auto Layout din interiorul celulei. Self-sizing reduce performanța pentru celule complexe — utilizați înălțime fixă (rowHeight) pentru tabele cu 500+ elemente.
UITableViewCell oferă patru stiluri încorporate care acoperă 80% din scenarii fără a fi nevoie să creați o celulă personalizată. Fiecare stil constă dintr-o combinație de textLabel, detailTextLabel și imageView. Alegerea stilului se setează la inițializarea celulei prin init(style:reuseIdentifier:).
| Stil | textLabel | detailTextLabel | imageView | Exemplu |
|---|---|---|---|---|
| .default (.basic) | Stânga, îngroșat | Nu | Opțional | Meniu, listă de elemente |
| .subtitle | Stânga, îngroșat | Sub textLabel, gri | Opțional | Contacte, liste de redare |
| .value1 | Stânga, îngroșat | Dreapta, gri | Nu | Setări cu valori |
| .value2 | Dreapta, albastru | Stânga, gri | Nu | Agendă telefonică (stil iOS 6) |
// Crearea unui tabel cu celule personalizate
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+) — modalitatea modernă de configurare a celulelor prin contentConfiguration în loc de accesarea directă a textLabel/detailTextLabel. Utilizați cell.defaultContentConfiguration() sau creați o UIContentConfiguration personalizată. Avantaje: suport pentru tip dinamic, temă întunecată și VoiceOut din cutie. Apple (WWDC 2024) recomandă contentConfiguration pentru toate UITableView noi.
UITableViewDataSource — protocol care furnizează date pentru tabel. Metode obligatorii: tableView(_:numberOfRowsInSection:) și tableView(_:cellForRowAt:). Fără ele tabelul nu poate afișa niciun rând. UITableViewDelegate — protocol pentru gestionarea aspectului și comportamentului: înălțimea rândurilor, selecție, editare, contextual actions.
// Exemplu cu mai multe secțiuni și editare
extension ContactListController: UITableViewDelegate {
// Înălțime personalizată pentru fiecare rând
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Gestionarea selecției
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Glisare pentru ștergere
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Șterge") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Performanța Delegate: metodele de dimensiune (heightForRowAt) sunt apelate pentru fiecare rând vizibil la fiecare derulare. Pentru înălțime uniformă setați rowHeight direct — aceasta este de 10 ori mai rapidă decât heightForRowAt. Pentru celule self-sizing setați estimatedRowHeight și automaticDimension — tabelul va apela heightForRowAt doar pentru rândurile vizibile, iar pentru restul va utiliza estimatedRowHeight.
Coada de reuse — pool de celule UITableView reutilizabile. Când o celulă iese din ecran, nu este ștearsă, ci plasată în coadă. Celulele noi nu sunt create de la zero — sistemul apelează dequeueReusableCell(withIdentifier:for:), care returnează o celulă din coadă sau creează una nouă dacă coada este goală. Acesta este mecanismul cheie de performanță al UITableView.
// Celulă personalizată cu cache
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)
// Configurarea constrângerilor Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constrângeri ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Încărcarea asincronă a avatarului
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Înregistrare și utilizare
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// În cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — metodă apelată înainte de plasarea celulei în coada de reuse. Resetați întreaga stare a celulei: text, imagini, animații, încărcări. Fără prepareForReuse, o celulă reutilizată poate afișa date vechi: imaginea anterioară va fi vizibilă pentru o fracțiune de secundă până când se încarcă una nouă. Apple (WWDC 2023) recomandă curățarea operațiilor asincrone prin URLSessionTask.cancel() în prepareForReuse.
Secțiunile în UITableView grupează rândurile logic conexe. Fiecare secțiune poate avea header (antet) și footer (subsol). Numărul de secțiuni este determinat de metoda numberOfSections(in:) (implicit 1). Pentru stilurile grouped și insetGrouped, secțiunile sunt vizual separate prin margini și colțuri rotunjite.
| Metoda DataSource | Descriere | Valoare returnată |
|---|---|---|
| numberOfSections | Numărul de secțiuni (implicit 1) | Int |
| numberOfRowsInSection | Numărul de rânduri în secțiune | Int |
| titleForHeaderInSection | Textul antetului secțiunii | String? |
| titleForFooterInSection | Textul subsolului secțiunii | String? |
| viewForHeaderInSection | Vizualizare personalizată pentru header | UIView? |
| heightForHeaderInSection | Înălțimea headerului | CGFloat |
Indexare: pentru navigare rapidă între secțiuni adăugați un index — un șir de caractere afișat în dreapta. Implementați sectionIndexTitles(for:) — returnează un array cu primele litere ale fiecărei secțiuni. Pentru 26+ secțiuni acest lucru îmbunătățește critic UX. În iOS 15+, tabelele suportă sectionHeaderTopPadding — marginea între bara de stare și primul header care poate fi resetată la 0 pentru un aspect compact.
Performanța UITableView este critică pentru aplicațiile cu liste lungi. Problemele principale: derulare lentă (jank) din cauza operațiilor grele în cellForRowAt, apeluri frecvente ale heightForRowAt, lipsa prefetching. Apple (WWDC 2025) evidențiază cinci practici cheie pentru tabele cu 1000+ elemente.
// Optimizare cu 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)
}
}
}
// Utilizarea DiffableDataSource pentru actualizări fluide
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)
}
}
Rezultat practic: aplicarea acestor optimizări crește FPS-ul de derulare de la 30 la 60 pe dispozitive iPhone 12 și mai vechi (conform testelor Apple, 2025). În proiectele IT Sectr, folosim DiffableDataSource pentru toate tabelele noi și adăugăm prefetching pentru tabelele cu imagini (avataruri, previzualizări, pictograme de aplicații).
Întrebări frecvente
Verificați: (1) contentSize > frame.size — tabelul trebuie să aibă mai mult conținut decât înălțimea sa; (2) tabelul nu se află în interiorul unui alt UIScrollView care interceptează gesturile; (3) isScrollEnabled = true; (4) numberOfRowsInSection returnează numărul corect. Problema tipică — tabel cu heightForRowAt = UITableView.automaticDimension fără estimatedRowHeight, ceea ce duce la calculul infinit al înălțimii.
Utilizați performBatchUpdates sau DiffableDataSource în loc de reloadData(). reloadData() provoacă reîncărcarea completă fără animație, creând pâlpâire vizuală. Pentru actualizare punctuală utilizați: reloadRows(at:with:), insertRows, deleteRows. Toate modificările din interiorul performBatchUpdates sunt animate simultan. DiffableDataSource face acest lucru automat fără apeluri manuale.
UITableView — listă verticală cu o coloană cu stiluri de celule predefinite și editare încorporată. UICollectionView — aspect flexibil (grile, listă, carusel, cascadă) cu amplasare arbitrară a elementelor. Alegeți UITableView pentru liste simple (setări, contacte, mesaje). Alegeți UICollectionView pentru galerii, cataloage, panouri și orice aspect neliniar.
Setați tableView.tableFooterView = UIView(frame: .zero). În mod implicit, UITableView afișează rânduri goale (separatoare) sub ultimul element până la marginea de jos. Setarea unui UIView gol în tableFooterView le elimină. Pentru stilul grouped acest lucru nu este necesar — tabelul afișează doar rândurile reale cu colțuri rotunjite ale ultimei secțiuni.
Setați tableView.rowHeight = 80 și tableView.estimatedRowHeight = 0 sau omiteți estimatedRowHeight. Cu înălțime fixă, UITableView nu apelează heightForRowAt, ceea ce accelerează randarea de 5–10 ori pentru liste cu 100+ elemente. Pentru celule cu înălțime personalizată (conținut diferit) utilizați self-sizing: estimatedRowHeight + UITableView.automaticDimension.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și