UITableView — основной компонент UIKit для отображения вертикальных списков в iOS. Каждая строка таблицы — это ячейка UITableViewCell с предопределёнными стилями. Объясняем основы UITableView: DataSource, делегат, переиспользование ячеек и производительность таблиц в iOS-приложениях. По данным Apple (Human Interface Guidelines, 2026), UITableView остаётся самым используемым UI-компонентом в iOS — 85% приложений из топ-100 App Store содержат хотя бы одну таблицу. Для сложных макетов читайте статью об UICollectionView.
Главное
UITableView — это класс из фреймворка UIKit, предназначенный для отображения прокручиваемого списка строк в одной колонке. Каждая строка представлена объектом UITableViewCell. UITableView появился в iPhone OS 2 (2008) и с тех пор остаётся основным компонентом для списков в iOS. Архитектура UITableView построена на паттерне MVC: данные управляются DataSource, внешний вид и поведение — Delegate, а представление — самой таблицей.
Два стиля таблицы: .plain (сплошной список с опциональными секциями) и .grouped (сгруппированные секции с закруглёнными углами и отступами). С iOS 13 добавлен .insetGrouped — grouped-стиль с отступами от краёв, используемый в приложениях Настройки и Здоровье. Выбор стиля влияет на внешний вид по умолчанию: plain-таблицы прикрепляют header секции к верху при скролле (sticky header), grouped — нет.
Начиная с iOS 8, UITableView поддерживает ячейки с автоматическим размером (self-sizing cells). Для включения установите tableView.estimatedRowHeight (например, 80) и tableView.rowHeight = UITableView.automaticDimension. Таблица автоматически вычисляет высоту строки на основе Auto Layout констреинтов внутри ячейки. Self-sizing снижает производительность для сложных ячеек — используйте фиксированную высоту (rowHeight) для таблиц с 500+ элементами.
UITableViewCell предоставляет четыре встроенных стиля, которые покрывают 80% сценариев без необходимости создавать кастомную ячейку. Каждый стиль состоит из комбинации textLabel, detailTextLabel и imageView. Выбор стиля задаётся при инициализации ячейки через init(style:reuseIdentifier:).
| Стиль | textLabel | detailTextLabel | imageView | Пример |
|---|---|---|---|---|
| .default (.basic) | Слева, жирный | Нет | Опционально | Меню, список пунктов |
| .subtitle | Слева, жирный | Под textLabel, серый | Опционально | Контакты, плейлисты |
| .value1 | Слева, жирный | Справа, серый | Нет | Настройки с значениями |
| .value2 | Справа, синий | Слева, серый | Нет | Телефонная книга (iOS 6-стиль) |
// Создание таблицы с кастомными ячейками
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.
UITableViewDataSource — протокол, который предоставляет данные для таблицы. Обязательные методы: tableView(_:numberOfRowsInSection:) и tableView(_:cellForRowAt:). Без них таблица не сможет отобразить ни одной строки. UITableViewDelegate — протокол для управления внешним видом и поведением: высота строк, выделение, редактирование, contextual actions.
// Пример с несколькими секциями и редактированием
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.
Reuse queue — пул переиспользуемых ячеек UITableView. Когда ячейка уходит за пределы экрана, она не удаляется, а помещается в очередь. Новые ячейки не создаются с нуля — система вызывает dequeueReusableCell(withIdentifier:for:), которая возвращает ячейку из очереди или создаёт новую, если очередь пуста. Это ключевой механизм производительности UITableView.
// Кастомная ячейка с кэшированием
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() — метод, вызываемый перед помещением ячейки в reuse queue. Сбросьте всё состояние ячейки: текст, изображения, анимации, загрузки. Без 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 для header | UIView? |
| heightForHeaderInSection | Высота header | CGFloat |
Индексация: для быстрой навигации по секциям добавьте index title — массив строк, отображаемых справа. Реализуйте sectionIndexTitles(for:) — возвращает массив из первых букв каждой секции. Для 26+ секций это критически улучшает UX. В iOS 15+ таблицы поддерживают sectionHeaderTopPadding — отступ между статус-баром и первым header, который можно сбросить до 0 для плотного макета.
Производительность UITableView критически важна для приложений с длинными списками. Основные проблемы: медленная прокрутка (jank) из-за тяжёлых операций в cellForRowAt, частые вызовы heightForRowAt, отсутствие prefetching. Apple (WWDC 2025) выделяет пять ключевых практик для таблиц с 1000+ элементами.
// Оптимизация с 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 для таблиц с изображениями (аватары, превью, иконки приложений).
Часто задаваемые вопросы
Проверьте: (1) contentSize > frame.size — таблица должна иметь больше контента, чем её высота; (2) таблица не внутри другого UIScrollView с перехватом жестов; (3) isScrollEnabled = true; (4) numberOfRowsInSection возвращает корректное количество. Типичная проблема — таблица с heightForRowAt = UITableView.automaticDimension без estimatedRowHeight, что приводит к бесконечному расчёту высоты.
Используйте performBatchUpdates или DiffableDataSource вместо reloadData(). reloadData() вызывает полную перезагрузку без анимации, создавая визульное мигание. Для точечного обновления используйте: reloadRows(at:with:), insertRows, deleteRows. Все изменения внутри performBatchUpdates анимируются одновременно. DiffableDataSource делает это автоматически без ручных вызовов.
UITableView — одноколоночный вертикальный список с предопределёнными стилями ячеек и встроенным редактированием. UICollectionView — гибкий макет (сетка, список, карусель, каскад) с произвольным расположением элементов. Выбирайте UITableView для простых списков (настройки, контакты, сообщения). Выбирайте UICollectionView для галерей, каталогов, досок и любых нелинейных макетов.
Установите tableView.tableFooterView = UIView(frame: .zero). По умолчанию UITableView отображает пустые строки (разделители) ниже последнего элемента до нижнего края. Установка пустого UIView в tableFooterView убирает их. Для grouped-стиля это не требуется — таблица отображает только фактические строки с закруглёнными углами последней секции.
Установите tableView.rowHeight = 80 и tableView.estimatedRowHeight = 0 или опустите estimatedRowHeight. С фиксированной высотой UITableView не вызывает heightForRowAt, что ускоряет рендер в 5–10 раз для списков из 100+ элементов. Для ячеек с кастомной высотой (разный контент) используйте self-sizing: estimatedRowHeight + UITableView.automaticDimension.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.