Делаем UITableView. Для начинающих

в 12:54, , рубрики: datasource, iOS, swift, tableview, UI, uikit, UITableView, UITableViewCell, uitableviewcontroller, uitableviewdatasource, xcode, разработка под iOS

Новогодние праздники прошли, а мое стремление писать полезные и не очень статьи — нет! Сегодня поговорим о UITableView, работе с UITableViewDataSource и переиспользовании ячеек. Затронем как установить рут контроллер без сториборда, ошибки при работе с таблицей, лейаут и большой заголовок для UINavigationBar.

Для тех, кому нравятся несмешные шутки, я записал ролик на YouTube. Ну а здесь всё будет серьезно. Давайте начнём.

Создадим пустой проект, назовем как душе угодно и перейдем в контроллер. В UIKit есть класс UITableViewController. Вы можете нагуглить много туториалов, где таблицу показывают именно в контексте этого класса. Но для большего понимания все сделаем в базовом UIViewController.

Чаще всего, когда нужна таблица, используется UINavigationController:

Делаем UITableView. Для начинающих - 1

Давайте добавим его. В файл AppDelegate, функцию didFinishLaunchingWithOptions вставим следующий код:

let navigationController = UINavigationController.init(rootViewController: ViewController())
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()

Короткий ликбез зачем: cториборды и констрейнты — добро, но в этом туториале постараемся обойтись без них. Этот код позволит игнорировать сториборд (его можно удалить) и обернёт ViewController в UINavigationController.

Хорошо бы установить заголовок для UINavigationBar. Для этого мы перейдем в класс ViewController (всю дальнейшую работу мы будем делать тут) и в метод viewDidLoad добавим следующий код:

self.view.backgroundColor = UIColor.white
self.navigationItem.title = "Table"
self.navigationController?.navigationBar.prefersLargeTitles = true

Теперь заголовок станет большим и модным. Запустив проект, увидим следующее:

Делаем UITableView. Для начинающих - 2

Делаем TableView

Подготовительные действия закончены, можем переходить к главному. В контроллере создадим проперти UITableView. Выберем инициализатор, который имеет параметр Style. Фрейм выставим любой, к нему мы вернемся потом. А для стиля установите Grouped. Почему такое название стиля — понятия не имею, но он позволит нам сделать нативную таблицу, как в приложении настройки. Если вам нужна не стилизованная таблица — используйте инициализатор только с параметром frame.

Делаем UITableView. Для начинающих - 3

Layout

Здесь бы нас спасли констрейнты, но не будем искать легкий путей. Покажу способ, который использую я. Он конечно не претендует на канонический, не обязательно делать именно так и что там говорят в таких случаях. Сделаем функцию, которая будет выставлять фрейм вьюхам, а параметром будет принимать размер контроллера:

private func updateLayout(with size: CGSize) {
   self.tableView.frame = CGRect.init(origin: .zero, size: size)
}

Вызвать функцию нужно в двух местах — в методе viewDidLoad и в viewWillTransition:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
   super.viewWillTransition(to: size, with: coordinator)
   coordinator.animate(alongsideTransition: { (contex) in
      self.updateLayout(with: size)
   }, completion: nil)
}

Теперь таблица при любой ориентации будет размещена на весь экран. В метод updateLayout можно добавлять другие обработки.

Делаем UITableViewCell

Так как туториал не о ячейках, а таблице, то подробно на кастомизации останавливаться не будем. Сделаем класс ячейки, наследуемая от базового:

class TableViewCell: UITableViewCell {

}

Что бы ячейку использовать в таблице, нужно зарегистрировать класс. Для этого во ViewController у таблицы вызоваем следующий метод:

self.tableView.register(TableViewCell.self, forCellReuseIdentifier: "TableViewCell")

Если с классом понятно, то идентификатор заслуживает внимания. В 99% случаем сработает правило:

— "Ячейки одного класса должны иметь один идентификатор"

Ситуаций, когда для ячеек одного класса нужны разные идентификаторы, крайне мало, и в основном они связаны с отрисовкой кодом и анимациями.

DataSource

Это проперти, которое указывает на объект, который будет наполнять таблицу. Т.е. реализовывать протокол UITableViewDataSource. Обязательными по умолчанию являются два метода:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}
    
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}

Первый отвечает за количество ячеек в секции. Cекция у нас пока одна, многосекцие (такое слово есть?) рассматривать не будем. Второй метод за получение объекта ячейки. Работает он хитро, не думайте что вы раскусили бойца!

Но сначала давайте добавим массив строк, которым будем наполнять таблицу.

Делаем UITableView. Для начинающих - 4

Теперь перейдем к реализации первого метода:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   switch tableView {
   case self.tableView:
      return self.data.count
    default:
      return 0
   }
}

Мы говорим таблице что в 0-ой секции будет ячеек столько, сколько элементов в массиве data. Второй метод чуть сложнее. Просто проинициализировать ячейку не получится, и всё из-за системы переиспользования ячеек. Но не нужно ругать Apple, на самом деле это хорошо! Чтобы получит объект, нужно вызывать следующий код:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = self.tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
   cell.textLabel?.text = self.data[indexPath.row]
   return cell
}

Метод dequeueReusableCell получит объект ячейки по идентификатору, а мы сделаем приведение типа при помощи as до класса TableViewCell, которым и должна быть ячейка. textLabel это проперти базового класса, никакой дополнительной настройки не требуется.

Остается указать dataSource для таблицы и метод viewDidLoad теперь должен выглядеть так:

override func viewDidLoad() {
   super.viewDidLoad()
   self.view.backgroundColor = UIColor.white
   self.navigationItem.title = "Table"
   self.navigationController?.navigationBar.prefersLargeTitles = true
        
   self.view.addSubview(self.tableView)
   self.tableView.register(TableViewCell.self, forCellReuseIdentifier: "TableViewCell")
   self.tableView.dataSource = self
        
   self.updateLayout(with: self.view.frame.size)
}

Если мы запустим проект, то увидим таблицу, наполненную контентом:

Делаем UITableView. Для начинающих - 5

Переиспользование

Пока кажется что всё в порядке. Давайте добавим для одной из ячеек disclosureIndicator. Это аксессуар, который вы точно встречали в iOS:

Делаем UITableView. Для начинающих - 6

Допустим стоит задача установить индикатор только для первой ячейки. Первое что придет на ум — добавить простой if в метод cellForRowAt:

if indexPath.row == 0 {
   cell.accessoryType = .disclosureIndicator
}

Но враг прячется! Давайте запустим проект, и проскролим таблицу за пределы экрана:

Делаем UITableView. Для начинающих - 7

Индикатор появился для первой, но бессистемно появляется и для других ячеек! Это и есть переиспользование в действии — берётся ячейка, которая удобнее лежит в памяти (отличное объяснение для новичков, правда?) и конфигурируется согласно методу. Иногда случается что вытягивается ячейка с индикатором. Как итог — имеем такой баг. Что делать?

Всё просто. Есть два варианта решения. Первый заключается в блоке else и по сути подразумевает под собой однозначное конфигурирование любой ячейки. Метод будет выглядеть так:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = self.tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
   cell.textLabel?.text = self.data[indexPath.row]
   if indexPath.row == 0 {
      cell.accessoryType = .disclosureIndicator
   } else {
      cell.accessoryType = .none
   }
   return cell
}

Есть другой способ — реализовать метод prepareForReuse у ячейки. Как видно из названия — метод вызывается перед переиспользованием. Все что нужно — сбросить ячейку до дефолта. Код выглядит так:

class TableViewCell: UITableViewCell {
    
    override func prepareForReuse() {
        super.prepareForReuse()
        self.accessoryType = .none
    }
}

C переиспользыванием связано много нюансов. К примеру, задача с загрузкой изображений для ячейки в фоне с пагинацией, адаптивная высота, анимированное удаление и вставка. Но об этом во второй части, если у меня дойдут руки)

Для ищущих

Я стараюсь регулярно записывать туториал на своем канале. Можно найти ролики как рисовать кодом или как сделать контроллер плеера Apple Music.

Автор: IvanVorobei

Источник


  1. Дима:

    Класс
    Благодарочка

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js