[Из песочницы] Зачем нужны UITableViewController и UICollectionViewController

Всем привет, меня зовут Артём, я iOS-разработчик. Сегодня хочу рассказать о подходах к использованию UITableViewController и UICollectionViewController.

Едва ли можно найти мобильное приложение, в котором не используется списочное представление данных. Существенную часть времени мы (iOS-разработчики) проводим с TableView или CollectionView. Именно поэтому критически важным является выбор подходов к использованию этих базовых элементов из соображений скорости разработки и стоимости дальнейшей поддержки создаваемых решений. Хочу поделиться выводами, к которым пришли с коллегами в Touch Instinct.

Статья рассчитана на разработчиков, которые работают с TableView (CollectionView), но почему-то не работают с TableViewController (CollectionViewController). Далее будет упоминаться только TableView (Controller), но все написанное касается и CollectionView (Controller) тоже.

Вариант 1. MassiveViewController


Самый простой и привычный для многих вариант — когда разработчик создает в storyboard«е ViewController, располагает на нём TableView и указывает ViewController в качестве объекта, который будет реализовывать протоколы UITableViewDelegate и UITableViewDataSource.
ff33251d9ab1406d9701e26ceb3512e6.png

Все хорошо, но не всегда. Порой, возникают проблемы. И возникают они, когда в данном контроллере появляется дополнительная логика, мало связанная с TableView. Ладно еще, если в контроллере появляются обработчики UIBarButtonItem«ов. Но, как правило, появляется в этом контроллере всё подряд: обработчики кнопок под (над/слева/справа) TabieView, различные всплывающие элементы, работа с сетью, конфигурирование ячеек и так далее. Налицо грубое нарушение принципов Single Responsibility. ViewController грустит… Еще больше грустят другие разработчики, когда видят такую картину.

Вариант 2. Sолидный


Рядового разработчика после первой встречи со статьями про SOLID начинают преследовать сомнительные идеи различной степени тяжести. Одна из таких идей заключается в повсеместном применении принципа Single Responsibility. В том числе, в ущерб здравому смыслу.

Под впечатлением от прочитанного разработчик создает отдельные объекты для реализации каждого протокола, как показано на схеме ниже.

7e58ff99526b44da8400e1a8f7cadc38.png

Но проблема такого подхода заключается в том, что UIKit зачастую затрудняет поддержку данного принципа. На деле получается, что тесно связанные между собой вещи оказываются по разную сторону баррикад. Элементарный пример: при расчете высоты ячейки (Delegate) необходимо знать её содержимое (DataSource). В конечном итоге всё может стать еще более запутанным, чем в предыдущем варианте.

Вариант 3. ПолуSолидный


Разработчик понял, что использование Sолидного варианта связано с определенными трудностями, и решил слегка упростить его. Для этого создается не два класса, а один, который реализует протоколы Delegate и DataSource. Данный вариант реализовать легче, но и у него есть свои недостатки, которые заключается в том, что все равно необходимо создать дополнительный класс и обеспечить двустороннюю связь между ViewController«ом и объектом, реализующим протоколы.
0f08cf9e127247888af614f65a4e81b7.png

На самом деле все просто


Когда-то давным-давно обсуждали эту проблему с коллегами. И тут один опытный разработчик сказал: «А что тут обсуждать-то? Все и так есть из коробки».

И ведь действительно есть. Почему-то многие разработчики не используют TableViewController, аргументируя это тем, что есть отдельный TableView, который можно положить на view, как захочется. На это хочется ответить двумя аргументами:

  1. Практически всегда TableView будет растягивается на всю view — не проще ли сразу использовать TableViewController?
  2. Если TableView будет расположен не на всю view, значит будут присутствовать другие элементы, а связанный с ними код появится в ViewController«е.

Нет ничего хорошего в том, что код TableView смешивается с кодом других элементов, за которые отвечает ViewController. А представьте насколько усугубляется картина, если речь идет об iPad приложении и на экране больше чем две таблицы, которые обрабатываются одним ViewController«ом.

Подход, который я предлагаю рассмотреть — компромисс между Sолидностью и простотой. Заключается он в отказе от отдельно лежащих TableView. Последние версии iOS и Xcode позволяют применять данный подход без боли и мучений, с удобством и удовольствием.

4ffc97c53fcd4b5d952490f32834817b.png

Многим придется не по вкусу идея плодить ViewController«ы, но на деле они будут появляться только там, где это действительно нужно. К примеру, стоит задача сделать таблицу на весь экран. Лучше сразу использовать для этой цели TableViewController. Если нужно добавить такую же таблицу куда-нибудь еще, то вы спокойно сможете переиспользовать данный TableViewController, т.к. в нём будет только относящаяся к таблице логика и ничего лишнего.

Если вдруг появилась необходимость изменить расположение TableView, то просто создается отдельный ViewController, в который интегрируется TableViewController (через ViewController Containment). Данное решение является настолько коробочным, что все можно сделать через storyboard:

c5c0489284ad4f7884db0c6a5c93dd50.png

А еще интегрированные ViewController«ы будут изменять свой размер в соответствии с контейнером, в который они интегрированы, что не может не радовать глаз и в очередной раз подтверждает «коробочность» данного решения.

Тоже самое можно сделать и в коде:

let embedController = UIViewController()
addChildViewController(embedController)
view.addSubview(embedController.view)
embedController.view.frame = view.bounds // Здесь можно использовать Auto Layout, но это совсем другая история
embedController.didMoveToParentViewController(self)

Специальная рубрика: вопрос на засыпку!

Почему не нужно вызывать embedController.willMoveToParentViewController(self)?

Правильный ответ
Данный метод вызывается внутри
addChildViewController(embedController)

Следует обратить внимание, что в случае удаления встроенного ViewController«а все происходит наоборот:
embedController.willMoveToParentViewController(nil)
embedController.view.removeFromSuperview()
embedController.removeFromParentViewController()

А метод embedController.didMoveToParentViewController(self) будет вызван автоматически.

Идем далее.

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

  • BarButtonItems. Их можно легко добавить в TableViewController и обработать там же. Для этого необходимо включить отображение NavigationBar«а в Simulated Metrics и добавить Navigation Item.
    39c955912cd6478bb163ac448190a47e.png

  • Шапка таблицы. Не все знают, что в TableView можно вставить header для всей таблицы, а не только для секций.
    a0dd6d9446124d5485c3afc49355b350.png

    Добавленная таким образом шапка будет скроллиться вместе с контентом таблицы.

Как теперь с этим жить работать?


Если из ParentViewController нужно что-то передать в ChildViewController, необходимо переопределить метод prepareForSegue.
private var someController: SomeViewController!

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let someController = segue.destinationViewController as? SomeViewController {
        self.someController = someController
    }
}

Ну и совсем уж очевидный совет напоследок


Не нужно забивать ViewController всякой дрянью. Логика, которая не относится к элементам на экране, должна быть вынесена. Хорошей практикой, о которой, к счастью, знают почти все, является вынесение кода конфигурирования ячеек в классы этих ячеек.
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    if let cell = cell as? SomeCell {
        cell.textLabel = someObject.someText // плохо
        cell.numberLabel = someObject.someNumber // плохо
        cell.configureForObject(someObject) // хорошо
    } else if let cell = cell as? OtherCell {
        cell.textLabel = otherObject.text // плохо
        cell.numberLabel = otherObject.number // плохо
        cell.configureForObject(otherObject) // хорошо
    }
}

Резюме


  • Не используем отдельный TableView
  • Используем TableViewController
  • Нужно добавить что-то в TableViewController — создаем ParentViewController
  • Не конфигурируем ячейки прямо во ViewController«е, а передаём модель в ячейку и производим конфигурацию там
  • Применяем все тоже самое и к CollectionView

Комментарии (0)

© Habrahabr.ru