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

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

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

Вариант 1. MassiveViewController


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


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

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


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

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


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

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


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



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


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

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

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

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

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


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

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


А еще интегрированные 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.


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


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

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


Если из 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
Поделиться с друзьями
-->

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


  1. AKhatmullin
    27.09.2016 10:34
    +2

    Что-то я не совсем понимаю в чем же в итоге смысл и выгода использования UITableViewController'а вместо комбо UIViewController + UITableView. Хорошо когда на экране нет совершенного ничего кроме этой таблицы, но это ведь далеко не всегда так.
    Представим такую экран: информация о неком профиле игрока в некой игре. В верхней части экрана у нас расположены юзернейм, аватар и еще какая-нибудь информация, а внизу уже таблица со статистикой/ачивками. Разве в этом случае логично использовать UITableViewController упакованный в контейнер? Получается вместо одного view controller'а на котором расположены разные элементы мы получаем view controller в который упакован контейнер в который упакован еще один view controller. Звучит как ненужное усложнение логики. Можно живой пример увидеть?


    1. artyomdevyatov
      28.09.2016 10:27

      Согласен с вами в том, что в статье не хватает живых примеров. Спасибо, добавлю. А пока вот:
      Большим плюсом подхода, о котором я рассказал в статье, является возможность разбивать функциональность на взаимозаменяемые части, которые потом очен удобно (пере)использовать.
      Давайте предположим, что для вашего примера использовальзуется UIViewController + UITableView. Внезапно прилетает задача и вы узнаете, что теперь необходимо «оптимизировать» данный экран для iPad, добавив справа от первого TableView второй (leaderboards, например). В такой ситуации вам придется внедрять реализацию протоколов для второго TableView рядом с реализацией для первого (даже не рядом, а прямо в том же самом месте).
      А теперь давайте представим, что используется встроенный UITableViewController. Можно в считанные минуты скопировать экран, рядом с одним контейнером (для левой TableView) положить второй (для правой TableView) и готово. Каждый TableView живет своей отдельной жизнью и может быть переиспользован на других экранах.
      Часто так бывает, что экран Leaderboards уже был в iPhone версии приложения и выглядил очень похоже: вверху общая информация, ниже TableView. В iPad версию нужно вставить только TableView. Если использовался встроенный UITableViewController, то можно сразу его и внедрить без необходимости вносить правки в код. Если использовался подход UIViewController + UITableView, то скорее всего придется менять код, чтобы он заработал для другого экрана, поскольку в нем с большой долей вероятности будут зависимости от первого экрана.


      1. AKhatmullin
        28.09.2016 10:59

        Так стало чуть яснее. Спасибо!
        Тем не менее практических примеров все равно жду.


  1. Makaveli
    27.09.2016 12:54

    Кроме общего хэдера для таблицы можно ещё разбить таблицу на секции — секция с одними данными, секция с другими данными.

    Разница между UITableViewController и UIViewController + UITableView совсем невелика, мне кажется тут всё больше вкусовщина. И переиспользовать можно через тот же Container.

    В Swift реализация протоколов с помощью extension очень приятно выглядит — сразу видно что где и разделение:

    // MARK: - UITableViewDataSource
    extension MyViewController: UITableViewDataSource {
    	
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: MyTableViewCell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellIdentifier", for: indexPath) as! MyTableViewCell
    		
        cell.someDataModel = MyDataModel
    		
        return cell
      }
    
    }
    


    Не конфигурируем ячейки прямо во ViewController’е, а передаём модель в ячейку и производим конфигурацию там

    Это вообще довольно очевидная вещь, по-моему, только вы не показали в статье — как надо :)


    1. artyomdevyatov
      28.09.2016 10:38

      Спасибо за дополнение. По поводу extensions:
      Когда используются UIViewController + UITableView, а протоколы реализуются в extensions, то очень часто появляются зависимости между основным классом контроллера и extensions. В итоге эти extensions нельзя просто взять и применить к другому контроллеру, на котором, возможно, появится та же таблица.
      Если используется отдельный (встроенный) контроллер, то разработчик будет писать модульный код, который потом очень легко переиспользовать.


  1. andrew8712
    27.09.2016 16:55
    +1

    >Это вообще довольно очевидная вещь, по-моему, только вы не показали в статье — как надо :)

    Это вообще довольно спорная вещь, где биндить данные :)


  1. Ariandr
    28.09.2016 09:48

    Интересный подход!
    Очевидный совет напоследок — не для всех очевидный, видел много примеров кода, где инициализация ячейки происходит в коде контроллера, причем там не по 3 строчки, а по 15. =]


    На самом деле, если функционал контроллера невелик — в Swift удобно реализовывать DataSource & Delegate как расширения и выносить в отдельный файл и код основного контроллера будет чистым и понятным. Собственно такая рекомендация в Swift Guidlines.


  1. Massmaker
    28.09.2016 09:48

    Спасибо за статью.
    Для себя вынес дополнительную пользу про

    embedded view controller
    
    , а так же правильность добавления/убирания его в коде