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. Предимства: поддръжка на динамичен тип, тъмна тема и VoiceOut веднага. 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)
}
// Плъзгане за изтриване
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 — група от повторно използваеми 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
// ... ограничения ...
}
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. Нулирайте цялото състояние на клетката: текст, изображения, анимации, зареждания. Без 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 | Персонализиран изглед за header | UIView? |
| heightForHeaderInSection | Височина на header | CGFloat |
Индексация: за бърза навигация между секциите добавете индекс — масив от низове, показван отдясно. Имплементирайте 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също