Создайте настоящую игру про футболиста для iOS, используя популярный архитектурный паттерн.

Мотивация

Прежде чем приступить к разработке приложения для iOS, мы должны подумать о структуре проекта. Нам нужно подумать, как мы добавляем эти фрагменты кода вместе, чтобы они имели смысл позже - когда мы вернемся и повторно посетим часть приложения - и как сформировать известный «язык» с другими разработчиками.

Это вторая статья в серии, и она полностью посвящена MVVM. (Ссылка на первую статью о MVC появляется в списке полезных ссылок в конце.)

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

Если вы просто хотите увидеть код, можете пропустить эту статью. Код доступен в виде открытого исходного кода на GitHub.

Зачем нужен шаблон архитектуры для вашего приложения iOS?

Самое важное, что нужно учитывать, - это иметь приложение, которое можно было бы поддерживать. Вы знаете, что представление идет туда, что этот контроллер представления должен выполнять X, а не Y. И что более важно, другие тоже это знают.

Вот некоторые преимущества выбора хорошего архитектурного паттерна:

  • Легче поддерживать
  • Легче проверить бизнес-логику
  • Выработайте общий язык с другими товарищами по команде
  • Разделите ответственность ваших сущностей
  • Меньше ошибок

Определение требований

Учитывая приложение iOS с шестью или семью экранами, мы собираемся разработать его с использованием самых популярных архитектурных шаблонов из мира iOS: MVC, MVVM, MVP, VIPER, VIP и координаторов.

Демо-приложение называется Football Gather и позволяет друзьям легко отслеживать результаты своих любительских футбольных матчей.

Основные особенности

Возможность:

  • Добавить игроков в приложение
  • Распределите команды по игрокам
  • Редактировать игроков
  • Установите таймер обратного отсчета для матчей

Мокапы экрана

Бэкэнд

Приложение работает на базе веб-приложения, разработанного в Веб-фреймворке Vapor. Вы можете ознакомиться с приложением в моей Начальной статье о Vapor 3 и статье о переходе на Vapor 4.

Что такое MVVM?

MVVM расшифровывается как Model-View-ViewModel, шаблон архитектуры, который естественно используется с RxSwift, где вы можете привязать свои элементы пользовательского интерфейса к классам модели через модель просмотра.

Это более новый шаблон, предложенный в 2005 году Джоном Госсманом и предназначенный для извлечения модели из контроллера представления. Взаимодействие между контроллером представления и моделью осуществляется через новый уровень, называемый моделью представления.

Модель

  • Модель - это тот же уровень, который был у нас в MVC, и используется для инкапсуляции данных и бизнес-логики.

Общение

  • Когда что-то происходит на уровне представления, например, когда пользователь инициирует действие, это передается модели через модель представления.
  • Когда модель изменяется, например, когда становятся доступными новые данные и нам нужно обновить пользовательский интерфейс, модель уведомляет View Model.

Вид

  • View и View Controller - это слои, на которых размещаются визуальные элементы.
  • Представление содержит элементы пользовательского интерфейса, такие как кнопки, метки и представления таблиц, а контроллер представления является владельцем представления.
  • Этот уровень такой же, как в MVC, но контроллер представления теперь является его частью и будет изменен для ссылки на модель представления.

Общение

  • Представления не могут напрямую взаимодействовать с моделью. Все делается через модель просмотра.

Посмотреть модель

  • Новый слой, который находится между View / View Controller и моделью.
  • Посредством привязки обновляет элементы пользовательского интерфейса, когда что-то изменилось в модели.
  • Каноническое представление представления
  • Предоставляет интерфейсы для просмотра

Общение

  • Может взаимодействовать с обоими уровнями, моделью и контроллером представления / представления.
  • Через привязку запускает изменения данных уровня модели.
  • При изменении данных проверяет, передаются ли эти изменения в пользовательский интерфейс, обновляя представление (снова через привязку).

Различные ароматы MVVM

Способ применения MVVM зависит от того, как вы решите реализовать привязку:

  • Использование стороннего поставщика, например RxSwift
  • KVO - наблюдение за ключом
  • Вручную

В нашем демонстрационном приложении мы рассмотрим ручной подход.

Как и когда использовать MVVM

Когда вы видите, что View Controller выполняет множество функций и может оказаться огромным, вы можете начать рассматривать различные шаблоны, такие как MVVM.

Преимущества

  • Уменьшает размер View Controller
  • Проще тестировать бизнес-логику, потому что теперь у вас есть специальный уровень, который обрабатывает данные.
  • Обеспечивает лучшее разделение проблем

Недостатки

  • То же, что и в MVC. Если он применяется неправильно и вы не заботитесь о SRP (принцип единой ответственности), он может превратиться в модель массового просмотра.
  • Может быть излишним и слишком сложным для небольших проектов (например, в приложении / прототипе Хакатона)
  • Использование стороннего поставщика увеличивает размер приложения и может повлиять на производительность.
  • Не кажется естественным разработка приложений для iOS с помощью UIKit. С другой стороны, для приложений, разработанных с помощью SwiftUI, это имеет смысл.

Внизу вы можете найти набор ссылок, которые расскажут вам больше об этом шаблоне архитектуры кода.

Применение MVVM к нашему коду

Это довольно просто. Мы заходим в каждый контроллер представления и извлекаем бизнес-логику на новый уровень (модель представления).

Отделение LoginViewController от бизнес-логики

Преобразования:

  • viewModel - новый слой, который обрабатывает состояние представления и обновления модели.
  • Сервисы теперь являются частью уровня модели представления.

В методе viewDidLoad мы вызываем функцию configureRememberMe(). Здесь мы можем наблюдать, как представление запрашивает у модели представления значения параметра «Запомнить меня» UISwitch и имени пользователя:

Для действий входа в систему и регистрации мы говорим модели представления обрабатывать запросы на обслуживание. Мы используем замыкания для обновления пользовательского интерфейса после завершения вызова API сервера.

LoginViewModel определяется следующими свойствами:

У нас есть службы, которые были переданы из LoginViewController (LoginService, StandardNetworkService, используемые для регистрации пользователя, и фасилитаторы хранилища: UserDefaults и Keychain оболочки).
Все они вводятся через инициализатор:

Это пригодится для модульного тестирования, если мы хотим использовать собственные фиктивные службы или хранилище.

Публичный API прост и понятен:

И два вызова API сервера:

Как видите, код выглядит намного чище, если модель отделена от контроллера представления. Теперь контроллер представления / представления запрашивает у модели представления, что ему нужно.

PlayerListViewController намного больше, его сложнее реорганизовать и извлечь из него бизнес-логику, чем LoginViewController.

Во-первых, мы хотим оставить только выходы и все UIView объекты, необходимые для этого класса.

В viewDidLoad мы выполним настройку и конфигурацию начального состояния представлений, установив делегат модели представления и запустив загрузку проигрывателя через модель представления.

Загрузка игроков:

Обработка ответа аналогична тому, что у нас есть в LoginViewController:

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

Чтобы удалить игрока, делаем следующее:

Переход к экранам «Подтвердить / Подробно» и «Добавить» осуществляется через performSegue. Мы выбираем PlayerListViewModel быть ответственным за создание моделей представления следующего экрана и внедряем их в prepareForSegue. Это не лучший подход, потому что мы нарушаем принцип SRP, но мы увидим в статье Координатора, как мы можем решить эту проблему.

PlayerListViewModel довольно большой и содержит множество свойств и методов, которые доступны View, все они обязательны.
Ради демонстрации мы оставим все как есть и оставим желаемый рефакторинг в качестве упражнения. для читателей. Ты мог бы:

  • отдельные PlayerListViewController в нескольких контроллерах представления / моделях представления, все они обрабатываются родительским или контейнерным контроллером представления.
  • разделить PlayerListViewModel на разные компоненты с помощью функций редактирования / вывода списка, служебного компонента или выбора проигрывателя.

Состояние просмотра (режимы выбора игрока и списка) реализуется с помощью заводского шаблона :

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

Методы обслуживания легко читаются:

PlayerAddViewController определяет экран добавления игроков

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

Сущность View Model представлена ​​ниже:

PlayerDetailViewController определяет экран "Подробности"

Модель представления создается и передается в методе PlayerListViewController, prepareForSegue. Мы используем тот же подход при переходе к PlayerEditViewController:

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

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

PlayerDetailViewModel имеет следующие свойства:

PlayerEditViewController

Переход к отображению экрана редактирования запускается с экрана PlayerDetails. Здесь вы можете редактировать данные об игроках.

Модель представления передается из PlayerDetailsViewController.
Следуя тому же подходу, мы переместили все взаимодействие API сервера, а также обработку модели, в модель представления.

Текстовое поле редактирования настраивается на основе свойств модели представления:

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

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

PlayerEditViewModel похож на остальные. Самыми важными методами будут методы обновления плеера:

ConfirmPlayersViewController

Прежде чем перейти на экран сбора, мы должны подтвердить выбранных игроков. Этот экран определяется ConfirmPlayersViewController.

В viewDidLoad мы настраиваем элементы пользовательского интерфейса, такие как представление таблицы, и настраиваем кнопку Start Gather:

Вызов серверного API представлен ниже:

И делегат представления таблицы и источник данных:

ConfirmPlayersViewModel содержит playersDictionary с выбранными игроками и их командами, сервисы, необходимые для добавления игроков в сборку и для начала сборки, gatherUUID, который определяется после создания сборки на сервере, и dispatchGroup для организации множественных вызовов сервера .

Самым сложным в этом классе является взаимодействие API сервера при запуске сборки:

GatherViewController

Наконец, у нас есть GatherViewController, относящийся к самому важному экрану Football Gather.

Нам удалось очистить свойства и оставить IBOutlets, а также представление загрузки и модель представления:

В viewDidLoad мы установили и настроили представления:

Функции, связанные с таймером, выглядят аккуратно:

И взаимодействие endGather API:

Источник данных и делегат табличного представления также выглядят великолепно, чисто и просто:

И остальные методы:

Очистка ViewController имела некоторые недостатки в классе View Model. У него много методов, а класс большой (около 200 строк кода).

Мы решили переместить взаимодействие таймера в новую структуру, названную GatherTimeHandler. В этой структуре мы представляем selectedTime, который устанавливается вне класса и имеет еще две переменные: таймер и переменную состояния (может быть остановлен, запущен или приостановлен).

В общедоступном API есть такие методы, как остановка, сброс и переключение таймера, а также decrementTime:

В целом, это выглядит намного лучше по сравнению с первой итерацией, когда мы реализовали приложение через MVC.

Тестирование нашей бизнес-логики

Самая важная часть - это модель представления. Здесь мы реализовали бизнес-логику.

Тестирование названия:

Тестирование форматированного текста метки таймера обратного отсчета:

Проверка текста заголовка действия, который должен быть «Пуск», «Возобновить» или «Пауза».

Мы придерживаемся того же подхода для паузы и запуска:

Для тестирования функции stopTimer мы имитируем работающую систему:

То же самое для resetTimer:

Делегаты pickerView и tableView очень легко протестировать. Ниже приведены примеры некоторых модульных тестов:

Для завершения сборки мы используем фиктивную конечную точку и модели. Проверяем, получен ли ответ true:

Чтобы проверить, включен ли таймер, мы используем MockViewModelDelegate:

И модульный тест:

По сравнению с тестированием ViewController в архитектуре MVC жизнь становится проще при тестировании уровня ViewModel. Модульные тесты легко писать, легче понимать и намного проще.

Ключевые метрики

Строки кода - просмотр контроллеров

Строки кода - просмотр моделей

Модульные тесты

Время сборки

Тесты проводились в iPhone 8 Simulator с iOS 14.3, с использованием Xcode 12.4 и на i9 MacBook Pro 2019.

Заключение

Наше приложение теперь преобразовано из MVC в MVVM. Мы добавили новый уровень для обработки бизнес-логики и отделения ее от контроллера представления, лучше разделив обязанности.

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

Однако при работе с UIKit в ваших проектах MVVM неестественен и труден для применения.

Глядя на ключевые показатели, мы можем отметить следующие наблюдения:

  • Мы значительно сократили количество строк кода в контроллерах представления на 607 строк кода.
  • С другой стороны, для написания моделей представлений нам потребовалось 1113 строк кода.
  • В целом мы добавили в приложение 506 строк кода и семь файлов.
  • Среднее время выполнения модульного теста немного ухудшилось: оно увеличилось на 5,1 секунды.
  • Покрытие кода, примененное к функции Gathers, увеличилось на 1,6%, достигнув в общей сложности 97,3%, что дает больше уверенности при принятии изменений и рефакторинге частей приложения, не нарушая существующая логика.
  • По сравнению с MVC, модульные тесты, охватывающие бизнес-логику, писать было намного проще.

В заключение, MVVM был забавным упражнением; теперь у нас есть гораздо более чистое приложение, и мы можем даже сказать, что оно менее подвержено ошибкам.

Спасибо, что остались до конца! У нас есть несколько полезных ссылок ниже.

Полезные ссылки