UITableView — o principal componente do UIKit para exibir listas verticais em iOS. Cada linha da tabela é uma célula UITableViewCell com estilos predefinidos. Explicamos os fundamentos da UITableView: DataSource, delegate, reutilização de células e desempenho de tabelas em aplicações iOS. De acordo com a Apple (Human Interface Guidelines, 2026), a UITableView continua a ser o componente de UI mais usado em iOS — 85% das aplicações no top 100 da App Store contêm pelo menos uma tabela. Para layouts complexos, leia o artigo sobre UICollectionView.
Pontos principais
UITableView é uma classe do framework UIKit projetada para exibir uma lista rolável de linhas numa única coluna. Cada linha é representada por um objeto UITableViewCell. A UITableView apareceu pela primeira vez no iPhone OS 2 (2008) e desde então continua a ser o principal componente para listas em iOS. A arquitetura da UITableView é baseada no padrão MVC: os dados são geridos pelo DataSource, a aparência e o comportamento pelo Delegate, e a apresentação pela própria tabela.
Dois estilos de tabela: .plain (lista contínua com secções opcionais) e .grouped (secções agrupadas com cantos arredondados e margens). O iOS 13 adicionou .insetGrouped — um estilo agrupado com margens nas bordas, usado nas aplicações Definições e Saúde. A escolha do estilo afeta a aparência padrão: as tabelas plain fixam os cabeçalhos das secções no topo ao rolar (sticky header), as grouped não.
A partir do iOS 8, a UITableView suporta células de tamanho automático (self-sizing cells). Para ativar, defina tableView.estimatedRowHeight (por exemplo, 80) e tableView.rowHeight = UITableView.automaticDimension. A tabela calcula automaticamente a altura da linha com base nas restrições de Auto Layout dentro da célula. O auto-dimensionamento reduz o desempenho para células complexas — use altura fixa (rowHeight) para tabelas com mais de 500 elementos.
UITableViewCell fornece quatro estilos incorporados que cobrem 80% dos cenários sem necessidade de criar uma célula personalizada. Cada estilo consiste numa combinação de textLabel, detailTextLabel e imageView. O estilo é escolhido ao inicializar a célula através de init(style:reuseIdentifier:).
| Estilo | textLabel | detailTextLabel | imageView | Exemplo |
|---|---|---|---|---|
| .default (.basic) | Esquerda, negrito | Nenhum | Opcional | Menu, lista de itens |
| .subtitle | Esquerda, negrito | Abaixo de textLabel, cinzento | Opcional | Contactos, listas de reprodução |
| .value1 | Esquerda, negrito | Direita, cinzento | Nenhum | Definições com valores |
| .value2 | Direita, azul | Esquerda, cinzento | Nenhum | Lista telefónica (estilo iOS 6) |
// Criação de uma tabela com células personalizadas
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+) — uma forma moderna de configurar células através de contentConfiguration em vez de aceder diretamente a textLabel/detailTextLabel. Use cell.defaultContentConfiguration() ou crie uma UIContentConfiguration personalizada. Vantagens: suporte incorporado para Dynamic Type, modo escuro e VoiceOver. A Apple (WWDC 2024) recomenda contentConfiguration para todas as novas UITableView.
UITableViewDataSource — um protocolo que fornece dados para a tabela. Métodos obrigatórios: tableView(_:numberOfRowsInSection:) e tableView(_:cellForRowAt:). Sem eles, a tabela não pode exibir nenhuma linha. UITableViewDelegate — um protocolo para gerir aparência e comportamento: altura das linhas, seleção, edição, ações contextuais.
// Exemplo com várias secções e edição
extension ContactListController: UITableViewDelegate {
// Altura personalizada para cada linha
func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat {
80
}
// Manipulação de seleção
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: "Eliminar") {
[weak self] _, _, completion in
self?.contacts.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Desempenho do Delegate: os métodos de tamanho (heightForRowAt) são chamados para cada linha visível em cada rolagem. Para altura uniforme, defina rowHeight diretamente — isto é 10 vezes mais rápido que heightForRowAt. Para células de tamanho automático, defina estimatedRowHeight e automaticDimension — a tabela chama heightForRowAt apenas para as linhas visíveis e usa estimatedRowHeight para as restantes.
Fila de reutilização — um conjunto de células UITableView reutilizáveis. Quando uma célula sai do ecrã, não é eliminada mas sim colocada na fila. Novas células não são criadas do zero — o sistema chama dequeueReusableCell(withIdentifier:for:), que devolve uma célula da fila ou cria uma nova se a fila estiver vazia. Este é o mecanismo chave de desempenho da UITableView.
// Célula personalizada com caching
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)
// Configuração de restrições de Auto Layout
avatarView.translatesAutoresizingMaskIntoConstraints = false
// ... constraints ...
}
func configure(with contact: Contact) {
nameLabel.text = contact.name
subtitleLabel.text = contact.phone
// Carregamento assíncrono de avatar
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
nameLabel.text = nil
subtitleLabel.text = nil
}
}
// Registo e utilização
tableView.register(ContactCell.self, forCellReuseIdentifier: "contactCell")
// Em cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactCell
cell.configure(with: contacts[indexPath.row])
return cell
prepareForReuse() — um método chamado antes de colocar uma célula na fila de reutilização. Redefina todo o estado da célula: texto, imagens, animações, operações de carregamento. Sem prepareForReuse, uma célula reutilizada pode mostrar dados antigos: a imagem anterior ficará visível por uma fração de segundo até que a nova seja carregada. A Apple (WWDC 2023) recomenda cancelar operações assíncronas através de URLSessionTask.cancel() em prepareForReuse.
Secções na UITableView agrupam linhas logicamente relacionadas. Cada secção pode ter um cabeçalho (header) e um rodapé (footer). O número de secções é determinado pelo método numberOfSections(in:) (o padrão é 1). Para os estilos grouped e insetGrouped, as secções são visualmente separadas por margens e cantos arredondados.
| Método do DataSource | Descrição | Valor de retorno |
|---|---|---|
| numberOfSections | Número de secções (padrão 1) | Int |
| numberOfRowsInSection | Número de linhas numa secção | Int |
| titleForHeaderInSection | Texto do título do cabeçalho da secção | String? |
| titleForFooterInSection | Texto do título do rodapé da secção | String? |
| viewForHeaderInSection | Vista personalizada para o cabeçalho | UIView? |
| heightForHeaderInSection | Altura do cabeçalho | CGFloat |
Indexação: para navegação rápida entre secções, adicione um título de índice — uma matriz de strings exibidas no lado direito. Implemente sectionIndexTitles(for:) — devolve uma matriz das primeiras letras de cada secção. Para 26+ secções, isto melhora criticamente a experiência do utilizador. No iOS 15+, as tabelas suportam sectionHeaderTopPadding — o padding entre a barra de estado e o primeiro cabeçalho, que pode ser redefinido para 0 para um layout compacto.
O desempenho da UITableView é crítico para aplicações com listas longas. Problemas comuns: rolagem lenta (jank) devido a operações pesadas em cellForRowAt, chamadas frequentes a heightForRowAt, falta de prefetching. A Apple (WWDC 2025) destaca cinco práticas chave para tabelas com mais de 1000 elementos.
// Otimização com 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)
}
}
}
// Uso de DiffableDataSource para atualizações suaves
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)
}
}
Resultado prático: a aplicação destas otimizações aumenta os FPS de rolagem de 30 para 60 em dispositivos iPhone 12 e posteriores (de acordo com dados de testes da Apple, 2025). Nos projetos da IT Sectr, usamos DiffableDataSource para todas as novas tabelas e adicionamos prefetching para tabelas com imagens (avatars, pré-visualizações, ícones de aplicações).
Perguntas frequentes
Verifique: (1) contentSize > frame.size — a tabela deve ter mais conteúdo que a sua altura; (2) a tabela não está dentro de outra UIScrollView que interceta gestos; (3) isScrollEnabled = true; (4) numberOfRowsInSection devolve o número correto. Um problema típico é uma tabela com heightForRowAt = UITableView.automaticDimension sem estimatedRowHeight, o que causa cálculo infinito de altura.
Use performBatchUpdates ou DiffableDataSource em vez de reloadData(). reloadData() causa uma recarga completa sem animação, criando cintilação visual. Para atualizações direcionadas use: reloadRows(at:with:), insertRows, deleteRows. Todas as alterações dentro de performBatchUpdates são animadas simultaneamente. O DiffableDataSource faz isto automaticamente sem chamadas manuais.
UITableView é uma lista vertical de coluna única com estilos de célula predefinidos e edição incorporada. UICollectionView é um layout flexível (grelha, lista, carrossel, cascata) com posicionamento arbitrário de elementos. Escolha UITableView para listas simples (definições, contactos, mensagens). Escolha UICollectionView para galerias, catálogos, quadros e qualquer layout não linear.
Defina tableView.tableFooterView = UIView(frame: .zero). Por padrão, a UITableView exibe linhas vazias (separadores) abaixo do último item até à borda inferior. Definir uma UIView vazia como tableFooterView remove-as. Para o estilo grouped isto não é necessário — a tabela exibe apenas as linhas reais com cantos arredondados na última secção.
Defina tableView.rowHeight = 80 e tableView.estimatedRowHeight = 0 ou omita estimatedRowHeight. Com altura fixa, a UITableView não chama heightForRowAt, o que acelera a renderização em 5–10 vezes para listas de mais de 100 itens. Para células com altura personalizada (conteúdo diferente), use auto-dimensionamento: estimatedRowHeight + UITableView.automaticDimension.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também