UITableView: основи, списки та комірки в додатках iOS

Автор: IT Sectr Опубліковано: 2026-02-23 Час читання: 9 хв

UITableView — основний компонент UIKit для відображення вертикальних списків в iOS. Кожен рядок таблиці — це комірка UITableViewCell із попередньо визначеними стилями. Пояснюємо основи UITableView: DataSource, делегат, перевикористання комірок та продуктивність таблиць у додатках iOS. За даними Apple (Human Interface Guidelines, 2026), UITableView залишається найбільш використовуваним UI-компонентом в iOS — 85% додатків із топ-100 App Store містять хоча б одну таблицю. Для складних макетів читайте статтю про UICollectionView.

Головне

  • UITableView — компонент UIKit для вертикальних списків, доступний з iOS 2.0 (iPhone OS 2).
  • UITableViewCell — комірка таблиці з чотирма вбудованими стилями та можливістю кастомізації.
  • UITableViewDataSource — протокол, що визначає кількість рядків та комірок для кожного розділу.
  • UITableViewDelegate — протокол для обробки вибору, редагування та налаштування висоти рядків.
  • UITableViewDiffableDataSource — сучасний DataSource (iOS 13+) з автоматичним розрахунком змін.

Що таке UITableView в iOS?

UITableView — це клас із фреймворку UIKit, призначений для відображення списку рядків, які можна прокручувати, в одній колонці. Кожен рядок представлений об'єктом UITableViewCell. UITableView вперше з'явився в iPhone OS 2 (2008) і відтоді залишається основним компонентом для списків в iOS. Архітектура UITableView побудована на шаблоні MVC: дані керуються DataSource, зовнішній вигляд і поведінка — Delegate, а представлення — самою таблицею.

Два стилі таблиці: .plain (суцільний список з опціональними розділами) та .grouped (згруповані розділи із закругленими кутами та відступами). iOS 13 додав .insetGrouped — grouped-стиль із відступами від країв, який використовується в додатках Налаштування та Здоров'я. Вибір стилю впливає на зовнішній вигляд за замовчуванням: plain-таблиці прикріплюють заголовок розділу до верху під час скролу (sticky header), grouped — ні.

Self-Sizing Cells

Починаючи з iOS 8, UITableView підтримує комірки з автоматичним розміром (self-sizing cells). Для ввімкнення встановіть tableView.estimatedRowHeight (наприклад, 80) та tableView.rowHeight = UITableView.automaticDimension. Таблиця автоматично обчислює висоту рядка на основі Auto Layout констреінтів всередині комірки. Self-sizing знижує продуктивність для складних комірок — використовуйте фіксовану висоту (rowHeight) для таблиць із 500+ елементами.

Стилі UITableViewCell: Basic, Subtitle, Value1, Value2

UITableViewCell надає чотири вбудовані стилі, які покривають 80% сценаріїв без необхідності створювати кастомну комірку. Кожен стиль складається з комбінації textLabel, detailTextLabel та imageView. Вибір стилю задається під час ініціалізації комірки через init(style:reuseIdentifier:).

СтильtextLabeldetailTextLabelimageViewПриклад
.default (.basic)Зліва, жирнийНемаєОпціональноМеню, список пунктів
.subtitleЗліва, жирнийПід textLabel, сірийОпціональноКонтакти, плейлисти
.value1Зліва, жирнийСправа, сірийНемаєНалаштування зі значеннями
.value2Справа, синійЗліва, сірийНемаєТелефонна книга (стиль iOS 6)
swift
// Створення таблиці з кастомними комірками
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+) — сучасний спосіб налаштування комірок через contentConfiguration замість прямого звернення до textLabel/detailTextLabel. Використовуйте cell.defaultContentConfiguration() або створіть кастомну UIContentConfiguration. Перевага: підтримка динамічного типу, темної теми та VoiceOver з коробки. Apple (WWDC 2024) рекомендує contentConfiguration для всіх нових UITableView.

DataSource та Delegate: обов'язкові методи

UITableViewDataSource — протокол, який надає дані для таблиці. Обов'язкові методи: tableView(_:numberOfRowsInSection:) та tableView(_:cellForRowAt:). Без них таблиця не зможе відобразити жодного рядка. UITableViewDelegate — протокол для керування зовнішнім виглядом та поведінкою: висота рядків, вибір, редагування, контекстні дії.

swift
// Приклад з кількома розділами та редагуванням
extension ContactListController: UITableViewDelegate {

    // Кастомна висота для кожного рядка
    func tableView(_ tableView: UITableView,
                         heightForRowAt indexPath: IndexPath) -> CGFloat {
        80
    }

    // Обробка виділення
    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: "Видалити") {
            [weak self] _, _, completion in
            self?.contacts.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
            completion(true)
        }
        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
}

Продуктивність Delegate: методи розміру (heightForRowAt) викликаються для кожного видимого рядка при кожному скролі. Для рівної висоти встановіть rowHeight безпосередньо — це в 10 разів швидше, ніж heightForRowAt. Для self-sizing комірок встановіть estimatedRowHeight та automaticDimension — таблиця викличе heightForRowAt тільки для видимих рядків, а для решти використовує estimatedRowHeight.

Перевикористання комірок: черга перевикористання та реєстрація

Черга перевикористання — пул комірок UITableView, які можна перевикористовувати. Коли комірка виходить за межі екрана, вона не видаляється, а поміщається в чергу. Нові комірки не створюються з нуля — система викликає dequeueReusableCell(withIdentifier:for:), яка повертає комірку з черги або створює нову, якщо черга порожня. Це ключовий механізм продуктивності UITableView.

swift
// Кастомна комірка з кешуванням
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)

        // Налаштування Auto Layout констреінтів
        avatarView.translatesAutoresizingMaskIntoConstraints = false
        // ... constraints ...
    }

    func configure(with contact: Contact) {
        nameLabel.text = contact.name
        subtitleLabel.text = contact.phone
        // Асинхронне завантаження аватара
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        avatarView.image = nil
        nameLabel.text = nil
        subtitleLabel.text = nil
    }
}

// Реєстрація та використання
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")

// В cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell

prepareForReuse() — метод, що викликається перед поміщенням комірки в чергу перевикористання. Скиньте весь стан комірки: текст, зображення, анімації, завантаження. Без prepareForReuse перевикористана комірка може показувати старі дані: попереднє зображення буде видно долю секунди, поки не завантажиться нове. Apple (WWDC 2023) рекомендує очищати асинхронні операції через URLSessionTask.cancel() у prepareForReuse.

Розділи, заголовок та підвал: групування даних

Розділи в UITableView групують логічно пов'язані рядки. Кожен розділ може мати заголовок (header) та підвал (footer). Кількість розділів визначається методом numberOfSections(in:) (за замовчуванням 1). Для grouped та insetGrouped стилів розділи візуально розділені відступами та закругленими кутами.

Метод DataSourceОписЗначення, що повертається
numberOfSectionsКількість розділів (за замовчуванням 1)Int
numberOfRowsInSectionКількість рядків у розділіInt
titleForHeaderInSectionТекст заголовка розділуString?
titleForFooterInSectionТекст підвалу розділуString?
viewForHeaderInSectionКастомний View для заголовкаUIView?
heightForHeaderInSectionВисота заголовкаCGFloat

Індексація: для швидкої навігації по розділах додайте index title — масив рядків, що відображаються справа. Реалізуйте sectionIndexTitles(for:) — повертає масив перших літер кожного розділу. Для 26+ розділів це критично покращує UX. В iOS 15+ таблиці підтримують sectionHeaderTopPadding — відступ між статус-баром та першим заголовком, який можна скинути до 0 для щільного макету.

Продуктивність UITableView: найкращі практики

Продуктивність UITableView критично важлива для додатків з довгими списками. Основні проблеми: повільна прокрутка (jank) через важкі операції в cellForRowAt, часті виклики heightForRowAt, відсутність prefetching. Apple (WWDC 2025) виділяє п'ять ключових практик для таблиць із 1000+ елементами.

  • Flat-height — використовуйте константну rowHeight замість heightForRowAt для всіх комірок. Різниця: 60 FPS vs 30 FPS для таблиці з 500 рядків.
  • Prefetching — реалізуйте UITableViewDataSourcePrefetching для асинхронного завантаження даних (зображення, API) до появи комірки на екрані.
  • Менше сабв'ю — кожен сабв'ю в contentView збільшує час рендеру. Використовуйте draw() для простої графіки (розділювачі, іконки) замість UIImageView.
  • Важкі операції в фоні — форматування дат, локалізація, розрахунки виконуйте в моделі, а не в cellForRowAt. Використовуйте NSCache для результатів.
  • DiffableDataSource — замініть reloadData() на apply(snapshot) — анімація відбувається безкоштовно, таблиця не перезавантажується повністю.
swift
// Оптимізація з 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)
        }
    }
}

// Використання DiffableDataSource для плавних оновлень
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)
    }
}

Практичний результат: застосування цих оптимізацій підвищує FPS прокрутки з 30 до 60 на пристроях iPhone 12 та старше (за даними тестів Apple, 2025). У проектах IT Sectr ми використовуємо DiffableDataSource для всіх нових таблиць і додаємо prefetching для таблиць із зображеннями (аватари, прев'ю, іконки додатків).

Часті запитання

UITableView не скролиться — що робити?

Перевірте: (1) contentSize > frame.size — таблиця повинна мати більше контенту, ніж її висота; (2) таблиця не всередині іншого UIScrollView з перехопленням жестів; (3) isScrollEnabled = true; (4) numberOfRowsInSection повертає коректну кількість. Типова проблема — таблиця з heightForRowAt = UITableView.automaticDimension без estimatedRowHeight, що призводить до нескінченного розрахунку висоти.

Як оновити UITableView без мигання?

Використовуйте performBatchUpdates або DiffableDataSource замість reloadData(). reloadData() викликає повне перезавантаження без анімації, створюючи візуальне мигання. Для точкового оновлення використовуйте: reloadRows(at:with:), insertRows, deleteRows. Всі зміни всередині performBatchUpdates анімуються одночасно. DiffableDataSource робить це автоматично без ручних викликів.

У чому різниця між UITableView та UICollectionView?

UITableView — одноколоночний вертикальний список із попередньо визначеними стилями комірок та вбудованим редагуванням. UICollectionView — гнучкий макет (сітка, список, карусель, каскад) з довільним розташуванням елементів. Вибирайте UITableView для простих списків (налаштування, контакти, повідомлення). Вибирайте UICollectionView для галерей, каталогів, дощок та будь-яких нелінійних макетів.

Як прибрати порожні рядки під контентом в UITableView?

Встановіть tableView.tableFooterView = UIView(frame: .zero). За замовчуванням UITableView відображає порожні рядки (розділювачі) нижче останнього елемента до нижнього краю. Встановлення порожнього UIView в tableFooterView прибирає їх. Для grouped-стилю це не потрібно — таблиця відображає тільки фактичні рядки із закругленими кутами останнього розділу.

Як зробити UITableView з постійною висотою?

Встановіть tableView.rowHeight = 80 та tableView.estimatedRowHeight = 0 або опустіть estimatedRowHeight. З фіксованою висотою UITableView не викликає heightForRowAt, що прискорює рендер у 5–10 разів для списків із 100+ елементів. Для комірок з кастомною висотою (різний контент) використовуйте self-sizing: estimatedRowHeight + UITableView.automaticDimension.

Підсумки

  • UITableView — основний компонент UIKit для вертикальних списків з архітектурою DataSource та Delegate.
  • UITableViewCell — 4 вбудовані стилі (.default, .subtitle, .value1, .value2) та кастомні через contentConfiguration.
  • Черга перевикористання — пул комірок, які можна перевикористовувати; register + dequeueReusableCell + prepareForReuse обов'язкові.
  • DiffableDataSource (iOS 13+) — типобезпечний DataSource з автоматичним розрахунком змін та анімацією.
  • Продуктивність: фіксована rowHeight, prefetching, менше сабв'ю, важкі операції в фоні.
  • 85% iOS-додатків топ-100 App Store використовують UITableView для відображення контенту.
  • Для сіток, каруселей та складних макетів використовуйте UICollectionView замість UITableView.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також