UITableView — основни компонент UIKit за приказ вертикалних листи у iOS. Сваки ред табеле је ћелија UITableViewCell са предефинисаним стиловима. Објашњавамо основе UITableView: DataSource, делегат, поновно коришћење ћелија и перформансе табела у iOS апликацијама. Према Apple (Human Interface Guidelines, 2026), UITableView остаје најкоришћенији UI компонент у iOS — 85% апликација из топ-100 App Store садржи бар једну табелу. За сложене распореде прочитајте чланак о UICollectionView.
Главне ствари
UITableView — је класа из frameworka 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)
}
// Превуци да избришеш
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
// ... ограничења ...
}
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 | Прилагођени приказ за 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође