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, включая производительность, память и сопровождение.

Sources

Related topics