UITableView — hlavní komponenta UIKit pro zobrazování vertikálních seznamů v iOS. Každý řádek tabulky je buňka UITableViewCell s předdefinovanými styly. Vysvětlujeme základy UITableView: DataSource, delegát, opakované použití buněk a výkon tabulek v iOS aplikacích. Podle Apple (Human Interface Guidelines, 2026) zůstává UITableView nejpoužívanější UI komponentou v iOS — 85% aplikací z top-100 App Store obsahuje alespoň jednu tabulku. Pro složitá rozvržení si přečtěte článek o UICollectionView.
Hlavní body
UITableView — je třída z frameworku UIKit určená pro zobrazení rolovatelného seznamu řádků v jednom sloupci. Každý řádek je reprezentován objektem UITableViewCell. UITableView se objevil v iPhone OS 2 (2008) a od té doby zůstává hlavní komponentou pro seznamy v iOS. Architektura UITableView je postavena na vzoru MVC: data jsou spravována DataSource, vzhled a chování Delegate a zobrazení samotnou tabulkou.
Dva styly tabulky: .plain (souvislý seznam s volitelnými sekcemi) a .grouped (seskupené sekce se zaoblenými rohy a okraji). Od iOS 13 byl přidán .insetGrouped — grouped-styl s okraji od okrajů, používaný v aplikacích Nastavení a Zdraví. Výběr stylu ovlivňuje výchozí vzhled: plain-tabulky přichytí záhlaví sekce nahoru při rolování (sticky header), grouped — ne.
Od iOS 8 UITableView podporuje buňky s automatickou velikostí (self-sizing cells). Pro aktivaci nastavte tableView.estimatedRowHeight (např. 80) a tableView.rowHeight = UITableView.automaticDimension. Tabulka automaticky vypočítá výšku řádku na základě omezení Auto Layout uvnitř buňky. Self-sizing snižuje výkon pro složité buňky — použijte pevnou výšku (rowHeight) pro tabulky s 500+ prvky.
UITableViewCell nabízí čtyři vestavěné styly, které pokrývají 80% scénářů bez nutnosti vytvářet vlastní buňku. Každý styl se skládá z kombinace textLabel, detailTextLabel a imageView. Výběr stylu se nastavuje při inicializaci buňky pomocí init(style:reuseIdentifier:).
| Styl | textLabel | detailTextLabel | imageView | Příklad |
|---|---|---|---|---|
| .default (.basic) | Vlevo, tučně | Ne | Volitelné | Menu, seznam položek |
| .subtitle | Vlevo, tučně | Pod textLabel, šedá | Volitelné | Kontakty, seznamy skladeb |
| .value1 | Vlevo, tučně | Vpravo, šedá | Ne | Nastavení s hodnotami |
| .value2 | Vpravo, modrá | Vlevo, šedá | Ne | Telefonní seznam (styl iOS 6) |
// Vytvoření tabulky s vlastními buňkami
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+) — moderní způsob konfigurace buněk pomocí contentConfiguration namísto přímého přístupu k textLabel/detailTextLabel. Použijte cell.defaultContentConfiguration() nebo vytvořte vlastní UIContentConfiguration. Výhody: podpora dynamického typu, tmavého motivu a VoiceOver hned po vybalení. Apple (WWDC 2024) doporučuje contentConfiguration pro všechny nové UITableView.
UITableViewDataSource — protokol, který poskytuje data pro tabulku. Povinné metody: tableView(_:numberOfRowsInSection:) a tableView(_:cellForRowAt:). Bez nich tabulka nemůže zobrazit žádný řádek. UITableViewDelegate — protokol pro správu vzhledu a chování: výška řádků, výběr, úpravy, contextual actions.
// Příklad s více sekcemi a úpravami
extension ContactListController: UITableViewDelegate {
// Vlastní výška pro každý řádek
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Zpracování výběru
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Přejetím smazat
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Smazat") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Výkon Delegate: metody velikosti (heightForRowAt) jsou volány pro každý viditelný řádek při každém rolování. Pro stejnou výšku nastavte přímo rowHeight — to je 10krát rychlejší než heightForRowAt. Pro self-sizing buňky nastavte estimatedRowHeight a automaticDimension — tabulka zavolá heightForRowAt pouze pro viditelné řádky a pro ostatní použije estimatedRowHeight.
Fronta reuse — fond znovu použitelných UITableView buněk. Když buňka opustí obrazovku, není smazána, ale umístěna do fronty. Nové buňky nejsou vytvářeny od nuly — systém volá dequeueReusableCell(withIdentifier:for:), která vrátí buňku z fronty nebo vytvoří novou, pokud je fronta prázdná. To je klíčový mechanismus výkonu UITableView.
// Vlastní buňka s ukládáním do mezipaměti
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)
// Nastavení omezení Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... omezení ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Asynchronní načítání avatara
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Registrace a použití
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// V cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — metoda volaná před umístěním buňky do fronty reuse. Resetujte celý stav buňky: text, obrázky, animace, načítání. Bez prepareForReuse může znovu použitá buňka zobrazovat stará data: předchozí obrázek bude viditelný na zlomek sekundy, dokud se nenačte nový. Apple (WWDC 2023) doporučuje čištění asynchronních operací pomocí URLSessionTask.cancel() v prepareForReuse.
Sekce v UITableView seskupují logicky související řádky. Každá sekce může mít header (záhlaví) a footer (zápatí). Počet sekcí je určen metodou numberOfSections(in:) (výchozí 1). Pro styly grouped a insetGrouped jsou sekce vizuálně odděleny okraji a zaoblenými rohy.
| Metoda DataSource | Popis | Návratová hodnota |
|---|---|---|
| numberOfSections | Počet sekcí (výchozí 1) | Int |
| numberOfRowsInSection | Počet řádků v sekci | Int |
| titleForHeaderInSection | Text záhlaví sekce | String? |
| titleForFooterInSection | Text zápatí sekce | String? |
| viewForHeaderInSection | Vlastní zobrazení pro header | UIView? |
| heightForHeaderInSection | Výška header | CGFloat |
Indexace: pro rychlou navigaci mezi sekcemi přidejte index — řetězec zobrazený vpravo. Implementujte sectionIndexTitles(for:) — vrací pole prvních písmen každé sekce. Pro 26+ sekcí to kriticky zlepšuje UX. V iOS 15+ tabulky podporují sectionHeaderTopPadding — mezeru mezi stavovým řádkem a prvním headerem, kterou lze resetovat na 0 pro kompaktní rozvržení.
Výkon UITableView je kritický pro aplikace s dlouhými seznamy. Hlavní problémy: pomalé rolování (jank) kvůli těžkým operacím v cellForRowAt, častá volání heightForRowAt, nedostatek prefetching. Apple (WWDC 2025) zdůrazňuje pět klíčových postupů pro tabulky s 1000+ prvky.
// Optimalizace pomocí 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)
}
}
}
// Použití DiffableDataSource pro plynulé aktualizace
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)
}
}
Praktický výsledek: použití těchto optimalizací zvyšuje FPS rolování z 30 na 60 na zařízeních iPhone 12 a starších (podle testů Apple, 2025). V projektech IT Sectr používáme DiffableDataSource pro všechny nové tabulky a přidáváme prefetching pro tabulky s obrázky (avatary, náhledy, ikony aplikací).
Často kladené otázky
Zkontrolujte: (1) contentSize > frame.size — tabulka musí mít více obsahu než svou výšku; (2) tabulka není uvnitř jiného UIScrollView, který zachycuje gesta; (3) isScrollEnabled = true; (4) numberOfRowsInSection vrací správný počet. Typický problém — tabulka s heightForRowAt = UITableView.automaticDimension bez estimatedRowHeight, což vede k nekonečnému výpočtu výšky.
Použijte performBatchUpdates nebo DiffableDataSource místo reloadData(). reloadData() způsobí úplné znovunačtení bez animace, vytváří vizuální blikání. Pro bodovou aktualizaci použijte: reloadRows(at:with:), insertRows, deleteRows. Všechny změny uvnitř performBatchUpdates jsou animovány současně. DiffableDataSource to dělá automaticky bez ručních volání.
UITableView — jednosloupcový vertikální seznam s předdefinovanými styly buněk a vestavěnou editací. UICollectionView — flexibilní rozvržení (mřížka, seznam, karusel, kaskáda) s libovolným umístěním prvků. Zvolte UITableView pro jednoduché seznamy (nastavení, kontakty, zprávy). Zvolte UICollectionView pro galerie, katalogy, tabule a jakákoli nelineární rozvržení.
Nastavte tableView.tableFooterView = UIView(frame: .zero). Ve výchozím nastavení UITableView zobrazuje prázdné řádky (oddělovače) pod posledním prvkem až ke spodnímu okraji. Nastavení prázdného UIView v tableFooterView je odstraní. Pro grouped-styl to není nutné — tabulka zobrazuje pouze skutečné řádky se zaoblenými rohy poslední sekce.
Nastavte tableView.rowHeight = 80 a tableView.estimatedRowHeight = 0 nebo vynechejte estimatedRowHeight. S pevnou výškou UITableView nevolá heightForRowAt, což urychluje vykreslování 5–10krát pro seznamy se 100+ prvky. Pro buňky s vlastní výškou (různý obsah) použijte self-sizing: estimatedRowHeight + UITableView.automaticDimension.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také