Ускорьте разработку табличных представлений в Swift 5

В этой статье мы узнаем, как сделать создание табличных представлений и их ячеек быстрее и проще. Мы сделаем это, используя протоколы, наследование и абстракцию. К концу статьи у вас будет готовый к работе механизм, который вы сможете свободно интегрировать в свои приложения для iOS.

Короче говоря, при работе над новым табличным представлением мы будем исходить из этого:

К этому:

Наш план в статье таков:

  1. Создайте повторно используемую библиотеку для работы с табличными представлениями.
  2. Создайте пример приложения на его основе.

Исходный код готовой библиотеки и примера приложения доступны внизу статьи. Без дальнейших церемоний, давайте начнем.

Давайте начнем

Во-первых, давайте определим протокол TableRowModel:

  1. Свойство identifier будет содержать идентификатор повторного использования для конкретного UITableViewCell.
  2. Закрытие onSelection будет вызываться, когда сработает метод didSelectRowAtIndexPath объекта UITableView.
  3. Мы предоставляем значение по умолчанию nil для замыкания в расширении протокола, поэтому ячейки реализуют это свойство только при необходимости.

Далее, давайте сделаем то же самое для представления UITableViewHeaderFooterView, создав TableHeaderFooterModel:

Здесь мы указываем только identifier в качестве требования.

Закончив представления UITableViewCell и UITableHeaderFooterView, давайте теперь смоделируем часть табличного представления:

  1. Так же, как любой раздел в UITableView может иметь UITableViewHeaderFooter, здесь у нас есть свойство TableHeaderFooterModel, которое мы создали ранее.
  2. Точно так же у нас есть массив строк, каждая из которых представляет UITableViewCell.
  3. Наконец, мы делаем то же самое для нижних колонтитулов разделов.
  4. Инициализатор установит значение nil для верхних и нижних колонтитулов по умолчанию.

Теперь, когда у нас есть все базовые представления, давайте создадим базовый класс UITableViewCell, от которого будут наследоваться все конкретные ячейки в наших приложениях:

  1. Мы реализуем инициализатор, который устанавливает selectionStyle в .none и вызывает метод setupSubviews().
  2. Мы определяем метод setupSubviews(), который необходимо переопределить в подклассах. Когда подкласс реализует этот метод, он будет автоматически вызван в инициализаторе, что уменьшит объем работы, которую мы должны выполнить.
  3. То же самое делаем с методом setup(with rowModel: TableRowModel). Когда мы хотим настроить ячейку с данными, мы будем вызывать этот метод за пределами конкретной ячейки.

Давайте закончим настройку базовых классов, реализовав BaseHeaderFooterView:

Как и в BaseTableViewCell, здесь у нас есть методы setupSubviews() и setup(with headerFooterModel) для переопределения подклассов.

Осталось сделать только одну вещь, чтобы закончить библиотеку: фактический объект, отвечающий за UITableViewTableDirector.

Реализация TableDirector

Во-первых, давайте создадим класс TableDirector с определенным в нем свойством UITableView:

  1. Мы сохраняем слабую ссылку на UITableView, так как нет необходимости в сильной ссылке, чтобы сохранить его живым: конкретный UIViewController сохранит UITableView.
  2. Мы устанавливаем для свойств dataSource и delegate tableview значение self.

Теперь нам нужно реализовать UITableViewDelegate и UITableViewDataSource, чтобы избежать их реализации в будущем:

Прежде чем мы перейдем к этим методам с содержимым, давайте создадим несколько свойств вместе со вспомогательной функцией для регистрации ячеек:

  1. Свойство sections будет содержать массив TableSection моделей. Как только свойство установлено, мы регистрируем все UITableViewCell и UITableViewHeaderFooterView, используя свойство identifier TableRowModel и TableHeaderFooterModel.
  2. Мы добавляем замыкания onCellSelection и onCellWillDisplay, чтобы потребители (особенно контроллеры представлений) могли реагировать на жизненный цикл ячейки и события, когда им это нужно.
  3. Вспомогательный метод classFromString используется внутри didSet свойства sections для получения конкретного идентификатора повторного использования UITableViewCell или UITableViewHeaderFooterView.

Теперь, когда все свойства установлены, давайте наполним эти методы UITableViewDataSource и UITableViewDelegate реальным содержимым.

Сначала начнем с методов источника данных:

  1. Как бы просто это ни было, в методах numberOfSections мы возвращаем count свойства sections.
  2. Точно так же в методе numberOfRowsInSection мы возвращаем count из rowModels в конкретном section.
  3. В методе cellForRowAt мы получаем соответствующий rowModel текущему IndexPath и удаляем из очереди BaseTableViewCell. Затем мы вызываем метод setup(with rowModel) для ячейки и возвращаем его.

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

  1. В методе didSelectRow мы вызываем замыкание onCellSelection TableDirector в дополнение к закрытию onSelection конкретного TableRowModel. У нас есть две версии для гибкости, поскольку иногда потребителю API будет интересен только выбранный IndexPath, а не фактическая модель. Имея обе версии, у нас есть более широкий спектр возможностей.
  2. В методе willDisplayCell мы запускаем замыкание onCellWillDisplay.
  3. В методе viewForHeader мы пытаемся получить headerModel для определенного IndexPath. Если он существует, мы удаляем конкретный BaseTableHeaderView из очереди, вызываем для него метод setup(with headerModel) и возвращаем его. В противном случае мы возвращаем nil.
  4. В методе viewForFooter мы делаем те же шаги, что и внутри метода viewForHeader.
  5. В методе willDisplayHeaderView мы устанавливаем визуальную конфигурацию для заголовка.
  6. Метод heightForHeader использует значение UITableView.automaticDimension, если для IndexPath существует модель заголовка. В противном случае используется высота 1.
  7. Аналогичная логика происходит внутри метода heightForFooter.

Наконец-то мы закончили реализацию библиотеки. Теперь пришло время использовать его в реальном приложении!

Пример приложения

Наше приложение представляет собой одноэкранное приложение, которое показывает UITableView, заполняющее доступное пространство:

Мы хотим заполнить табличное представление данными, поэтому давайте сначала создадим конкретные TableRowModels.

Начнем с ячейки, показывающей метки слева и справа от нее:

  1. Мы указываем, что модель должна быть reuseIdentifier из LeftRightTitlesTableViewCell.
  2. Мы определяем свойства leftTitle, rightTitle, которые будут отображаться в leftLabel и rightLabel соответственно. Кроме того, мы добавляем свойство onSelection TableRowModel, так как мы будем реагировать на нажатия именно на эту ячейку.
  3. Внутри LeftRightTitlesTableViewCell мы присваиваем значения модели leftLabel и rightLabel внутри setup(with rowModel) метода.
  4. Метод setupSubviews() отвечает только за макет и ограничения.

Создав один тип ячейки, давайте создадим другой:

  1. Модель получает указание использовать reuseIdentifier TitleSwitchTableViewCell.
  2. Добавлены свойства title, isOn и onSwitchValueChanged, чтобы установить заголовок для метки, начальное состояние UISwitch и реагировать на изменения значения переключателя.
  3. В методе setup(with rowModel) мы настраиваем ячейку так же, как и раньше.
  4. Метод stateSwitchHandler срабатывает при изменении значения переключателя и вызывает onSwitchValueChanged закрытие модели.]
  5. Метод setupSubviews размещает подпредставления и устанавливает ограничения.

Прежде чем мы начнем интегрировать ячейки в UITableView, давайте для полноты картины также реализуем UITableViewHeaderFooterView:

  1. Модель будет использовать reuseIdentifier TitleHeaderView.
  2. В методе setup(with headerModel) мы настраиваем метку с текстом.
  3. Метод setupSubviews() размещает метку и устанавливает ограничения.

Большой! Ячейки и заголовок готовы. Теперь мы собираемся интегрировать их в UITableView:

  1. Мы инициализируем отложенное свойство TableDirector в файле ViewController.
  2. Добавьте свойство isSwitchOn для хранения текущего значения переключателя в реализуемом TitleSwitchTableViewCell.

Единственный шаг для того, чтобы табличное представление заработало, — это передать TableDirector разделов. Давайте сделаем именно это:

  1. Мы вызываем метод setupSections() (который мы сейчас создадим) в viewDidLoad().
  2. Метод setupSections() устанавливает разделы для файла tableDirector.
  3. Метод createFirstSection() создает раздел с заголовком и тремя LeftRightTitlesTableViewCell.
  4. Чтобы сделать создание LeftRightTitlesTableViewCell более быстрым и удобным для повторного использования, используется метод createLeftRightCellModel(leftTitle:rightTitle:). Он реализует базовое замыкание onSelection, которое будет печатать leftTitle и rightTitle каждый раз при выборе ячейки.
  5. Метод createSecondSection() создает еще один раздел с заголовком и один TitleSwitchTableViewCell. Он также реализует замыкание onSwitchValueChanged, которое обновляет свойство isSwitchOn объекта ViewController.

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

Отныне, чтобы создать представление таблицы с ячейками, нам нужно будет использовать только эти шаги:

  1. Создайте бетон TableRowModel.
  2. Создайте бетон BaseTableViewCell.
  3. Добавьте свойство TableDirector к ViewController и назначьте ему табличное представление.
  4. Установите свойство разделов TableDirector.

Ресурсы

Исходный код примера приложения доступен на GitHub. Кроме того, я подготовил готовый к использованию пакет Swift, доступ к которому вы можете получить здесь.

Я надеюсь, что вы нашли этот урок полезным, и теперь вам потребуется меньше времени для работы с UITableViews и UITableViewCells.

Спасибо за прочтение!