Cocoa MVC (осторожно много картинок)

mvc

#1

Основное понятие Cocoa MVC сформулировано здесь.

MVC состоит из трёх объектов (model view controller), определяет их роли и поведение … Это и так все знают (или могут погуглить), не будем на этом останавливаться.

Я хотел бы заострить внимание на контроллере, точнее на их видах и обратить внимание вот на какой момент:

Combining Roles
One can merge the MVC roles played by an object, making an object, for example, fulfill both the controller and view roles—in which case, it would be called a view controller. In the same way, you can also have model-controller objects. For some applications, combining roles like this is an acceptable design.

То есть, можно комбинировать роли! Обеднять вид и контроллер (view-controller) и модель и контроллер (model-controller).

Именно этот момент приводит многих “знатоков” MVC в замешательство (видимо не все об этом знают, или просто упускают из вида). Начинают разглагольствовать на тему что Cocoa MVC не MVC а какой-то “Massive View Controller”, начинают приписывать свойства MVC к MVP и т.д.

Предполагаю по их мнению Cocoa MVC выглядет так:

На самом деле всё проще :slight_smile:

Получаем три вида контроллеров (или больше, у кого на сколько фантазии хватит) и три сценария их использования:

  1. Писать всё во вью контроллере.
  2. Сделать параллельно к вью контроллеру модель контролер.
  3. Сделать “чистый” контроллер.

Первый самый простой и всем известный.
Второй я частично затрагивал здесь (там правда на другом акцент, но контроллер есть).
На третьем как раз и остановимся.

В iOS широко представлены вью контроллеры и других практически нет, видимо от того что у каждого контроллера почти всегда есть root view и вполне логично их объединить, но это дает как свои плюсы так и минусы.

Из плюсов - многое можно делать во вью контроллере не плодить лишних вью, это в свою очередь ускоряет и упрощает разработку.
Из минусов - не соответствует “правилам хорошего кода”, сложно тестировать, сложно повторно использовать (хотя это от части относится ко всем контроллерам).

В macOS есть обычные контроллеры (пример здесь):

И достаточно широко представлены:

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

##Пример

Конечно же пример, как без него :slight_smile:
Попробуем сделать «чистый» контроллер и не просто контроллер, а контроллер который можно протестировать и повторно использовать.

Поехали!

Xcode -> New Project -> Single View App:

Use Core Data и Include Unit Tests:

Создаём файл ManagedObjectContext.swift, в нем пишем:

Расширяем возможности NSManagedObjectContext (шаблон декоратор) теперь он синглтон и еще практически билдер (получаем синглтон от NSPersistentContainer).

Удаляем всё лишнее из AppDelegate:

И ViewController.swift заодно.

В xcdatamodel создадим сущность Event с атрибутом timestamp:

Смотрим что бы класс автоматически генерировался:

Events будут выводится на экран предположительно с помощью UITableView а может и не только и для взаимодействия с этим TableView нам нужен более универсальный Delegate и DataSource, без TableView:

ItemProtocol содержит только строку которая будет выводится на экран:

Создадим CollectionInterface, именно с этим универсальным интерфейсом будет работать контроллер:

Всё это пишем в один файл Collection.swift.

Далее расширяем наш Event с помощью ItemProtocol (в файле Event.swift):

Переходим к виду

Создадим TableViewCell:

У TableViewCell будет item из которого он будет брать текст и отдавать лейблу. Обратите внимание хоть мы и расширили нашу модель Event c помощью ItemProtocol, ItemProtocol от этого не становится моделью Event, ItemProtocol некая абстрактная сущность содержащая текст (подобный подход продемонстрирован в книге “Cocoa Design Patterns”).

И TableView:

Чтобы не плодить аутлеты создадим один который сразу и DataSource и Delegate. Адаптируем протоколы (шаблон адаптер) TableViewDataSource и TableViewDelegate под CollectionDataSource и CollectionDelegate и сам TableView под CollectionInterface.

Приводить всё к интерфейсам и работать непосредственно с ними, а не напрямую с объектами хорошая практика. Как минимум позволяет писать более гибкий код.

MainController:

Тут всё ровно тоже самое что обычно пишут во вью контроллер, с той лишь разницей что наследуемся не от UIViewController а от NSFetchedResultsController и нет вью а некий абстрактный CollectionInterface. CollectionInterface вместо TableView по двум причинам, шаблон bridge и mock-объект, но об этом чуть позже.

Пришло время перейти в сториборд (точнее в интерфейс билдер, но не суть) и связать всё воедино.

Но перед тем всё же придется создать TableViewController и воспользоваться его editButtonItem, возможно самый короткий TableViewController в мире :slight_smile:

Добавляем UITableViewController, подключаем к нему UINavigationController, у TabeViewCell пишем Identifier “Cell” и в navigation bar добавляем кнопку “add” (Bar Button Item):

Добавляем Object:

После чего выставляем классы.

У контроллера TableViewController:

У UITableView -> TableView:

У UITableViewCell -> TableViewCell:

У Object -> MainController:

У TableView отключаем аутлеты delegate и dataSource:

Теперь самое интересное.

Выбираем MainConntroller:

И подключаем кнопку:

Далее нужно связать TableView и MainContoller и тут возникают сложности т.к. интерфейс билдер не дружит с протоколами и дженериками, по этому у TableView временно закомментируем controller и напиши controller: NSObject:

В MainController закомментируем Event:

После чего можно подключить:

Возвращаем всё назад:

Теперь в MainController времмено закомментируем collection:

Подключим аутлет collection:

И сразу удалим его раскомментировав старый:

Должно получится так:

Можно запускать:

Detail

Создаем DetailController подкласс NSObject с переменной item:

Теперь нам этот item нужно откуда-то взять, нас выручит шаблон observer:

Есть observers которые MapTable, что позволят хранить слабые ссылки, в методе add: добавляем любой NSObject по ключу название метода. При инициализации в цикле forEach у всех observers вызываем методы по ключу и в качестве параметра передаём self.

В MainController подписываемся на сообщения (DetailController.add:) и в методе load передаём в detailController item по indexPath.

Переходим в сториборд.

Подключаем ViewController:

Добавляем UILabel и Object, выставляем у Object класс DetailController:

Подключаем лейбл:

Создаем файл Label.swift в нём пишем:

Протокол LabelInterface это некий абстрактный лейбл от которого нам нужен только текст, с этим LabelInterface мы будем работать. Расширяем UILabel с помощью LabelInterface.

Возвращаемся в DetailController и вместо UILabel пишем LabelInterface, в didSet выставляем текст из item:

Нам теперь неважно label это UILabel или еще что, главное что он LabelInterface.

И опять какой-то интерфейс, думаю к концу этой истории станет понято зачем :slight_smile:

Можно запустить и проверить как работает.

UICollectionView

Вдруг захотелось нам добавить возможность переключать вид из TableView в CollectionView (собственно к этому всё и шло). И если бы мы всё писали во вью контроллер, нам бы пришлось создавать UICollectionViewController и фактически дублировать код, но мы так не сделали и это оказалось большим плюсом :wink: всё что нам нужно это адаптировать CollectionView по аналогии c TableView, что позволит использовать тот же самый MainController (это как раз и будет тот шаблон мост).

Создаем CollectionViewCell практически такой же как TableViewCell:

Создаем CollectionView и адаптируем его:

Переходим в сториборд.

Отключаем TableViewController от NavigationController как root и подключаем как Custom:

У TableViewController возвращаем navigation bar:

Добавляем UICollectioViewController в него перетаскиваем Object, в Simulated Metrics Top Bar ставим Translucent Navigation Bar, в navigation bar добаляем кнопку “add” и в UICollectionViewCell добаляем UILabel пишем Identifier “Cell”:

Выставляем классы:

Подключаем лейбл:

У CollectionView отключаем dataSource и delegate:

Подключаем кнопку:

Тем же незатейливым способом что и в случае с TableView, соединяем MainController и CollectionView, должно получится так:

Соединяем NavigationController с CollectionViewController, а CollectionViewController c ViewController, так же как у TableViewController, получаем:

Осталось добавить возможность переключения между контролерами.

Для этого создаем NavigationController подкласс UINavigationController:

Я надеюсь тут ничего объяснять не надо (и так уже 63 картинки :scream:).

Создаем NavigationSegue и в его шаблонном методе perform пишем:

Мы точно знаем что source это UINavigationController и у этого контролера выставляем наш destination.

В сториборде у UINavigationController указываем класс NavigationController:

У кастомных сигвеев соответственно:

Запускаем:

Это был небольшой пример как можно повторно использовать контроллер :blush: и если вы еще здесь пришло время написать тест.

Тест

Создаём MockLabel, где проверяем что текст не nil:

MockCollection где selected номер выбранного item, в insertItem проверяем что текст не nil:

В классе MVCTests в методе setUp, удаляем все Items из DB:

Мы будем вставлять и удалять объекты в цикле, у нас после каждой вставки/удаления сохраняется контекст, сохранение происходит не в одно мгновение и нам нужно будет как то это отслеживать.

Прежде всего нужно сохранять асинхронно, можно через DispatchQueue.main.async, но лучше воспользоваться стандартным preform, для этого в MainController:

И сам тест (явно не самый лучший вариант, но в качестве примера я думаю сойдёт):

  • сollection - mock объект который имитирует (или пытается) поведение заложенное в CollectionInteface.
  • count - количество объектов.
  • numberOfObject - номер объекта.

В цикле forEach вставляем новый item и увеличивает numberOfObject на единицу.
Сразу спускаемся в уведомление NSManagedObjectContextDidSave, которое срабатывает после каждого сохранения. В блоке уведомления ждем когда numberOfObject будет равен count (все вставленные объекты сохранены), запускаем блок detail.
В detail проверяем что mainController возвращает numberOfItems равное count, в цикле выставляем номер выбранного объекта (как будто мы на него нажали), создаём DetailController и проверяем что у него item не nil и создаем MockLabel который проверяет текст.
После detail вызываем delete, в блоке которого уменьшает номер объекта на единицу и удаляет item по индексу. В блоке уведомления как numberOfObject равен нулю завершаем тест.

Если у вас что то не работает, проект здесь.

Надеюсь что данный пример будет так же полезен как все мои примеры :slight_smile: :slight_smile: :slight_smile:


#2

А вот знаю что буду читать на досуге!


#3

У меня выходит

Use of undeclared type ‘Event’


#4

А вот знаю что буду читать на досуге!


#5

Моя программа для macOS и я не использую Storyboards. У меня в одном NSView содержится NSTabView. В каждом Tab выполняются разные задачи, иногда происходит обмен данными между Tabs. Для операций во всех Tabs я использую один NSViewController. Конечно все что можно вынесено из NSViewController, в нем присутствует только то что связано с UI . Вынесено в различные extensions, не знаю насколько это правильный подход, на мой взгляд это удобно. Что неправильно в таком подходе? Какие контроллеры следовало бы создать ?


#6

Контроллеры можно делить по ответственности, например:

Контроллер делает что-то там с файлами:

Контроллер отвечает за поиск:

Используйте storyboards, в macOS есть bindings можно вообще без ViewControllers обходится. Нет правильного подхода, делайте так как удобно. Если код не дублируется, его можно повторно использовать, у него мало зависимостей, тогда подход правильный)


#7

#8

Спасибо. Еще вопрос. Равнозначны ли два подхода по эффективности использования памяти: использование метода как extension для String (value type), или тот же метод из отдельного кастомного класса (reference type). Плодить классы не хочется.


#9

Равнозначны (20 символов)