UITableView: podstawy, listy i komórki w aplikacjach iOS

Autor: IT Sectr Opublikowano: 2026-02-23 Czas czytania: 9 min

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 — komponent UIKit dla pionowych list, dostępny od iOS 2.0 (iPhone OS 2).
  • UITableViewCell — komórka tabeli z czterema wbudowanymi stylami i możliwością dostosowania.
  • UITableViewDataSource — protokół definiujący liczbę wierszy i komórek dla każdej sekcji.
  • UITableViewDelegate — protokół do obsługi zaznaczania, edycji i ustawiania wysokości wierszy.
  • UITableViewDiffableDataSource — nowoczesny DataSource (iOS 13+) z automatycznym obliczaniem zmian.

Czym jest UITableView w iOS?

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.

Self-Sizing Cells

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.

Style UITableViewCell: basic, subtitle, value1, value2

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:).

StyltextLabeldetailTextLabelimageViewPrzykład
.default (.basic)Po lewej, pogrubionyBrakOpcjonalnieMenu, lista pozycji
.subtitlePo lewej, pogrubionyPod textLabel, szaryOpcjonalnieKontakty, playlisty
.value1Po lewej, pogrubionyPo prawej, szaryBrakUstawienia z wartościami
.value2Po prawej, niebieskiPo lewej, szaryBrakKsiążka telefoniczna (styl iOS 6)
swift
// 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.

DataSource i Delegate: obowiązkowe metody

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.

swift
// 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.

Ponowne użycie komórek: kolejka reuse i register

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.

swift
// 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, header i footer: grupowanie danych

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 DataSourceOpisWartość zwracana
numberOfSectionsLiczba sekcji (domyślnie 1)Int
numberOfRowsInSectionLiczba wierszy w sekcjiInt
titleForHeaderInSectionTekst nagłówka sekcjiString?
titleForFooterInSectionTekst stopki sekcjiString?
viewForHeaderInSectionNiestandardowy widok dla headerUIView?
heightForHeaderInSectionWysokość headerCGFloat

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: najlepsze praktyki

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.

  • Flat-height — używaj stałej rowHeight zamiast heightForRowAt dla wszystkich komórek. Różnica: 60 FPS vs 30 FPS dla tabeli z 500 wierszami.
  • Prefetching — zaimplementuj UITableViewDataSourcePrefetching do asynchronicznego ładowania danych (obrazy, API) przed pojawieniem się komórki na ekranie.
  • Mniej subview — każdy subview w contentView zwiększa czas renderowania. Używaj draw() dla prostej grafiki (separatory, ikony) zamiast UIImageView.
  • Ciężkie operacje w tle — formatowanie dat, lokalizację, obliczenia wykonuj w modelu, a nie w cellForRowAt. Używaj NSCache do wyników.
  • DiffableDataSource — zastąp reloadData() na apply(snapshot) — animacja odbywa się bezpłatnie, tabela nie przeładowuje się w całości.
swift
// 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

UITableView nie przewija się — co robić?

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.

Jak zaktualizować UITableView bez migania?

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ń.

Jaka jest różnica między UITableView a UICollectionView?

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.

Jak usunąć puste wiersze pod treścią w UITableView?

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.

Jak zrobić UITableView ze stałą wysokością?

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

  • UITableView — główny komponent UIKit dla pionowych list z architekturą DataSource i Delegate.
  • UITableViewCell — 4 wbudowane style (.default, .subtitle, .value1, .value2) i niestandardowe przez contentConfiguration.
  • Kolejka reuse — pula ponownie używanych komórek; register + dequeueReusableCell + prepareForReuse obowiązkowe.
  • DiffableDataSource (iOS 13+) — typowo bezpieczny DataSource z automatycznym obliczaniem zmian i animacją.
  • Wydajność: stała rowHeight, prefetching, mniej subview, ciężkie operacje w tle.
  • 85% aplikacji iOS z top-100 App Store używa UITableView do wyświetlania treści.
  • Dla siatek, karuzel i złożonych układów używaj UICollectionView zamiast UITableView.

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.

Omów projekt

Przeczytaj również