UITableView — główny komponent UIKit do wyświetlania pionowych list w iOS. Każdy wiersz tabeli to komórka UITableViewCell z predefiniowanymi stylami. Wyjaśniamy podstawy UITableView: DataSource, delegat, ponowne użycie komórek i wydajność tabel w aplikacjach iOS. Według Apple (Human Interface Guidelines, 2026), UITableView pozostaje najczęściej używanym komponentem UI w iOS — 85% aplikacji z top-100 App Store zawiera co najmniej jedną tabelę. W przypadku złożonych układów przeczytaj artykuł o UICollectionView.
Najważniejsze
UITableView — to klasa z frameworka UIKit przeznaczona do wyświetlania przewijanej listy wierszy w jednej kolumnie. Każdy wiersz jest reprezentowany przez obiekt UITableViewCell. UITableView pojawił się w iPhone OS 2 (2008) i od tego czasu pozostaje głównym komponentem dla list w iOS. Architektura UITableView opiera się na wzorcu MVC: dane są zarządzane przez DataSource, wygląd i zachowanie przez Delegate, a widok przez samą tabelę.
Dwa style tabeli: .plain (ciągła lista z opcjonalnymi sekcjami) i .grouped (zgrupowane sekcje z zaokrąglonymi rogami i odstępami). Od iOS 13 dodano .insetGrouped — grouped-style z odstępami od krawędzi, używany w aplikacjach Ustawienia i Zdrowie. Wybór stylu wpływa na domyślny wygląd: plain-tabele przypinają header sekcji do góry podczas przewijania (sticky header), grouped — nie.
Od iOS 8 UITableView obsługuje komórki z automatycznym rozmiarem (self-sizing cells). Aby włączyć, ustaw tableView.estimatedRowHeight (np. 80) i tableView.rowHeight = UITableView.automaticDimension. Tabela automatycznie oblicza wysokość wiersza na podstawie ograniczeń Auto Layout wewnątrz komórki. Self-sizing zmniejsza wydajność w przypadku złożonych komórek — używaj stałej wysokości (rowHeight) dla tabel z 500+ elementami.
UITableViewCell oferuje cztery wbudowane style, które pokrywają 80% scenariuszy bez potrzeby tworzenia niestandardowej komórki. Każdy styl składa się z kombinacji textLabel, detailTextLabel i imageView. Wybór stylu ustawia się podczas inicjalizacji komórki przez init(style:reuseIdentifier:).
| Styl | textLabel | detailTextLabel | imageView | Przykład |
|---|---|---|---|---|
| .default (.basic) | Po lewej, pogrubiony | Brak | Opcjonalnie | Menu, lista pozycji |
| .subtitle | Po lewej, pogrubiony | Pod textLabel, szary | Opcjonalnie | Kontakty, playlisty |
| .value1 | Po lewej, pogrubiony | Po prawej, szary | Brak | Ustawienia z wartościami |
| .value2 | Po prawej, niebieski | Po lewej, szary | Brak | Książka telefoniczna (styl iOS 6) |
// Tworzenie tabeli z niestandardowymi komórkami
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+) — nowoczesny sposób konfiguracji komórek przez contentConfiguration zamiast bezpośredniego odwoływania się do textLabel/detailTextLabel. Użyj cell.defaultContentConfiguration() lub utwórz niestandardową UIContentConfiguration. Zalety: obsługa typu dynamicznego, ciemnego motywu i VoiceOver od razu. Apple (WWDC 2024) zaleca contentConfiguration dla wszystkich nowych UITableView.
UITableViewDataSource — protokół, który dostarcza dane dla tabeli. Obowiązkowe metody: tableView(_:numberOfRowsInSection:) i tableView(_:cellForRowAt:). Bez nich tabela nie będzie w stanie wyświetlić żadnego wiersza. UITableViewDelegate — protokół do zarządzania wyglądem i zachowaniem: wysokość wierszy, zaznaczanie, edycja, contextual actions.
// Przykład z wieloma sekcjami i edycją
extension ContactListController: UITableViewDelegate {
// Niestandardowa wysokość dla każdego wiersza
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Obsługa zaznaczenia
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let contact = contacts[indexPath.row]
showDetail(for: contact)
}
// Przesunięcie do usunięcia
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath)
-> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Usuń") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Wydajność Delegate: metody rozmiaru (heightForRowAt) są wywoływane dla każdego widocznego wiersza przy każdym przewijaniu. Dla równej wysokości ustaw rowHeight bezpośrednio — to 10 razy szybsze niż heightForRowAt. Dla self-sizing komórek ustaw estimatedRowHeight i automaticDimension — tabela wywoła heightForRowAt tylko dla widocznych wierszy, a dla pozostałych użyje estimatedRowHeight.
Kolejka reuse — pula ponownie używanych komórek UITableView. Gdy komórka opuszcza ekran, nie jest usuwana, ale umieszczana w kolejce. Nowe komórki nie są tworzone od zera — system wywołuje dequeueReusableCell(withIdentifier:for:), która zwraca komórkę z kolejki lub tworzy nową, jeśli kolejka jest pusta. To kluczowy mechanizm wydajności UITableView.
// Niestandardowa komórka z buforowaniem
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)
// Konfiguracja ograniczeń Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... ograniczenia ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Asynchroniczne ładowanie awatara
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Rejestracja i użycie
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// W cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — metoda wywoływana przed umieszczeniem komórki w kolejce reuse. Zresetuj cały stan komórki: tekst, obrazy, animacje, ładowania. Bez prepareForReuse ponownie używana komórka może wyświetlać stare dane: poprzedni obraz będzie widoczny przez ułamek sekundy, dopóki nie załaduje się nowy. Apple (WWDC 2023) zaleca czyszczenie operacji asynchronicznych przez URLSessionTask.cancel() w prepareForReuse.
Sekcje w UITableView grupują logicznie powiązane wiersze. Każda sekcja może mieć header (nagłówek) i footer (stopkę). Liczba sekcji jest określana metodą numberOfSections(in:) (domyślnie 1). Dla stylów grouped i insetGrouped sekcje są wizualnie oddzielone odstępami i zaokrąglonymi rogami.
| Metoda DataSource | Opis | Wartość zwracana |
|---|---|---|
| numberOfSections | Liczba sekcji (domyślnie 1) | Int |
| numberOfRowsInSection | Liczba wierszy w sekcji | Int |
| titleForHeaderInSection | Tekst nagłówka sekcji | String? |
| titleForFooterInSection | Tekst stopki sekcji | String? |
| viewForHeaderInSection | Niestandardowy widok dla header | UIView? |
| heightForHeaderInSection | Wysokość header | CGFloat |
Indeksacja: dla szybkiej nawigacji po sekcjach dodaj indeks — tablicę ciągów wyświetlanych po prawej stronie. Zaimplementuj sectionIndexTitles(for:) — zwraca tablicę pierwszych liter każdej sekcji. Dla 26+ sekcji to krytycznie poprawia UX. W iOS 15+ tabele obsługują sectionHeaderTopPadding — odstęp między paskiem statusu a pierwszym headerem, który można zresetować do 0 dla zwartego układu.
Wydajność UITableView jest krytyczna dla aplikacji z długimi listami. Główne problemy: wolne przewijanie (jank) z powodu ciężkich operacji w cellForRowAt, częste wywołania heightForRowAt, brak prefetching. Apple (WWDC 2025) wyróżnia pięć kluczowych praktyk dla tabel z 1000+ elementami.
// Optymalizacja z 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)
}
}
}
// Użycie DiffableDataSource do płynnych aktualizacji
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)
}
}
Praktyczny wynik: zastosowanie tych optymalizacji zwiększa FPS przewijania z 30 do 60 na urządzeniach iPhone 12 i starszych (według testów Apple, 2025). W projektach IT Sectr używamy DiffableDataSource dla wszystkich nowych tabel i dodajemy prefetching dla tabel z obrazami (awatar, podgląd, ikony aplikacji).
Często zadawane pytania
Sprawdź: (1) contentSize > frame.size — tabela musi mieć więcej treści niż jej wysokość; (2) tabela nie znajduje się wewnątrz innego UIScrollView przechwytującego gesty; (3) isScrollEnabled = true; (4) numberOfRowsInSection zwraca poprawną liczbę. Typowy problem — tabela z heightForRowAt = UITableView.automaticDimension bez estimatedRowHeight, co prowadzi do nieskończonego obliczania wysokości.
Użyj performBatchUpdates lub DiffableDataSource zamiast reloadData(). reloadData() powoduje pełne przeładowanie bez animacji, tworząc wizualne miganie. Do punktowej aktualizacji użyj: reloadRows(at:with:), insertRows, deleteRows. Wszystkie zmiany wewnątrz performBatchUpdates są animowane jednocześnie. DiffableDataSource robi to automatycznie bez ręcznych wywołań.
UITableView — jednokolumnowa pionowa lista z predefiniowanymi stylami komórek i wbudowaną edycją. UICollectionView — elastyczny układ (siatka, lista, karuzela, kaskada) z dowolnym rozmieszczeniem elementów. Wybieraj UITableView dla prostych list (ustawienia, kontakty, wiadomości). Wybieraj UICollectionView dla galerii, katalogów, tablic i dowolnych nieliniowych układów.
Ustaw tableView.tableFooterView = UIView(frame: .zero). Domyślnie UITableView wyświetla puste wiersze (separatory) poniżej ostatniego elementu aż do dolnej krawędzi. Ustawienie pustego UIView w tableFooterView usuwa je. Dla stylu grouped nie jest to wymagane — tabela wyświetla tylko rzeczywiste wiersze z zaokrąglonymi rogami ostatniej sekcji.
Ustaw tableView.rowHeight = 80 i tableView.estimatedRowHeight = 0 lub pomiń estimatedRowHeight. Ze stałą wysokością UITableView nie wywołuje heightForRowAt, co przyspiesza renderowanie 5–10 razy dla list z 100+ elementami. Dla komórek z niestandardową wysokością (różna treść) użyj self-sizing: estimatedRowHeight + UITableView.automaticDimension.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również