UIKitJuniorTechnical
Как работает UITableView и каковы роли dataSource и delegate?
UITableViewDataSource предоставляет данные (количество строк, ячейки через dequeue), UITableViewDelegate обрабатывает действия (выбор строки, высота, swipe-действия); разделение обязанностей делает таблицу расширяемой.
UITableView: dataSource и delegate
UITableView — компонент UIKit для отображения вертикального скроллируемого списка ячеек. Он не хранит данные сам — вместо этого делегирует два набора обязанностей через протоколы: UITableViewDataSource (откуда брать данные) и UITableViewDelegate (как реагировать на действия).
UITableViewDataSource
Обязательные методы протокола:
numberOfRowsInSection— сколько строк в секции.cellForRowAt— создать или переиспользовать ячейку для indexPath.
Необязательные: количество секций, заголовки, перемещение и удаление строк.
UITableViewDelegate
Реагирует на действия пользователя и запрашивает параметры отображения:
didSelectRowAt— нажатие на строку.heightForRowAt— высота конкретной ячейки.trailingSwipeActionsConfigurationForRowAt— swipe-действия.
Полный пример
import UIKit
struct City {
let name: String
let population: String
}
class CitiesViewController: UIViewController {
private let tableView = UITableView(frame: .zero, style: .insetGrouped)
private var cities: [City] = [
City(name: "Москва", population: "12.6M"),
City(name: "Санкт-Петербург", population: "5.5M"),
City(name: "Новосибирск", population: "1.6M")
]
override func viewDidLoad() {
super.viewDidLoad()
title = "Города"
setupTableView()
}
private func setupTableView() {
tableView.dataSource = self
tableView.delegate = self
// Регистрация ячейки по идентификатору
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CityCell")
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
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)
])
}
}
// MARK: - UITableViewDataSource
extension CitiesViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int { 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// dequeueReusableCell возвращает существующую ячейку из пула — экономит память
let cell = tableView.dequeueReusableCell(withIdentifier: "CityCell", for: indexPath)
let city = cities[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = city.name
content.secondaryText = city.population
cell.contentConfiguration = content
return cell
}
}
// MARK: - UITableViewDelegate
extension CitiesViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let city = cities[indexPath.row]
print("Выбран: \(city.name)")
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
// Swipe-to-delete
func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Удалить") { [weak self] _, _, completion in
self?.cities.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
completion(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
Кастомная ячейка
class CityTableViewCell: UITableViewCell {
static let reuseIdentifier = "CityTableViewCell"
let nameLabel = UILabel()
let populationLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// Auto Layout setup...
}
required init?(coder: NSCoder) { fatalError() }
func configure(with city: City) {
nameLabel.text = city.name
populationLabel.text = city.population
}
}
// Регистрация
tableView.register(CityTableViewCell.self, forCellReuseIdentifier: CityTableViewCell.reuseIdentifier)
// В cellForRowAt:
let cell = tableView.dequeueReusableCell(withIdentifier: CityTableViewCell.reuseIdentifier, for: indexPath) as! CityTableViewCell
cell.configure(with: cities[indexPath.row])
Подводные камни
- Не вызывать
tableView.register(_:forCellReuseIdentifier:)передdequeueReusableCell— метод вернёт nil и при force-unwrap произойдёт краш. - Изменять данные массива без синхронного вызова
insertRows/deleteRows/reloadData— таблица рассчитывает, что numberOfRows совпадает с реальным массивом; несоответствие вызывает NSInternalInconsistencyException. - Не вызывать
deselectRow(at:animated:)в didSelectRowAt — строка остаётся визуально выделенной. - Использовать
cellForRow(at:)(UITableView) вместоcellForRowAt(dataSource) — метод возвращает nil для off-screen ячеек, что ведёт к пропущенным обновлениям UI. - Хранить состояние в ячейке — при переиспользовании состояние сохраняется для новой строки; всё состояние должно быть в модели данных.
- Устанавливать
automaticDimensionдля высоты строк без задания estimatedRowHeight — это вызывает многократные пересчёты layout и заметно замедляет прокрутку.
Common mistakes
- Сводить «работает UITableView и каковы роли
dataSourceиdelegate» к синтаксису и не объяснять main thread. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии uikit-6.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «работает UITableView и каковы роли
dataSourceиdelegate» и подтверждает ее корректным примером. - Умеет связать ответ с Auto Layout, тестированием и отладкой на устройстве.
- Называет ограничения подхода uikit-6, включая производительность, память и сопровождение.