Основное понятие 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 выглядет так:
На самом деле всё проще
Получаем три вида контроллеров (или больше, у кого на сколько фантазии хватит) и три сценария их использования:
- Писать всё во вью контроллере.
- Сделать параллельно к вью контроллеру модель контролер.
- Сделать “чистый” контроллер.
Первый самый простой и всем известный.
Второй я частично затрагивал здесь (там правда на другом акцент, но контроллер есть).
На третьем как раз и остановимся.
В iOS широко представлены вью контроллеры и других практически нет, видимо от того что у каждого контроллера почти всегда есть root view и вполне логично их объединить, но это дает как свои плюсы так и минусы.
Из плюсов - многое можно делать во вью контроллере не плодить лишних вью, это в свою очередь ускоряет и упрощает разработку.
Из минусов - не соответствует “правилам хорошего кода”, сложно тестировать, сложно повторно использовать (хотя это от части относится ко всем контроллерам).
В macOS есть обычные контроллеры (пример здесь):
И достаточно широко представлены:
Если вы с ними не знакомы, то советую ознакомится, хотя бы для того что бы понять что это и как работает.
##Пример
Конечно же пример, как без него
Попробуем сделать «чистый» контроллер и не просто контроллер, а контроллер который можно протестировать и повторно использовать.
Поехали!
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 в мире
Добавляем 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.
И опять какой-то интерфейс, думаю к концу этой истории станет понято зачем
Можно запустить и проверить как работает.
UICollectionView
Вдруг захотелось нам добавить возможность переключать вид из TableView в CollectionView (собственно к этому всё и шло). И если бы мы всё писали во вью контроллер, нам бы пришлось создавать UICollectionViewController и фактически дублировать код, но мы так не сделали и это оказалось большим плюсом всё что нам нужно это адаптировать 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 картинки ).
Создаем NavigationSegue и в его шаблонном методе perform пишем:
Мы точно знаем что source это UINavigationController и у этого контролера выставляем наш destination.
В сториборде у UINavigationController указываем класс NavigationController:
У кастомных сигвеев соответственно:
Запускаем:
Это был небольшой пример как можно повторно использовать контроллер и если вы еще здесь пришло время написать тест.
Тест
Создаём 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 равен нулю завершаем тест.
Если у вас что то не работает, проект здесь.
Надеюсь что данный пример будет так же полезен как все мои примеры