Создайте настоящую игру про футболиста для 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.
Нам удалось очистить свойства и оставить IBOutlet
s, а также представление загрузки и модель представления:
В 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 был забавным упражнением; теперь у нас есть гораздо более чистое приложение, и мы можем даже сказать, что оно менее подвержено ошибкам.
Спасибо, что остались до конца! У нас есть несколько полезных ссылок ниже.
Полезные ссылки
- Приложение для iOS, Football Gather - GitHub Repo Link
- Приложение веб-сервера, сделанное в Vapor - GitHub Repo Link
- API-интерфейсы Vapor 3 Backend ссылка на статью
- Переход на Vapor 4 ссылка на статью
- Model View Controller (MVC) - Ссылка на репозиторий GitHub и ссылка на статью
- Модель View ViewModel (MVVM) - Ссылка на репозиторий GitHub и ссылка на статью
- Model View Presenter (MVP) - ссылка на репозиторий GitHub и ссылка на статью
- Шаблон координатора - MVP с координаторами (MVP-C) - ссылка на репозиторий GitHub и ссылка на статью
- View Interactor Presenter Entity Router (VIPER) - ссылка на репозиторий GitHub и ссылка на статью
- View Interactor Presenter (VIP) - ссылка на репозиторий GitHub и ссылка на статью
- Книга о МВВМ по Райвендерлиху.
- Статья о MVVM в iOS
- Статья« Как не отчаяться с внедрением MVVM »
- Введение в шаблон Модель / Представление / Модель представления для создания приложений WPF
- MVVM vs MVC
- Использование MVV в iOS
- Практичный MVVM + RxSwift
- MVVM с RxSwift
- Как интегрировать RxSwift в вашу архитектуру MVVM
- Каковы преимущества Model-View-ViewModel
- Преимущества шаблона MVVM - Преимущества использования модели MVVM
- Преимущества и недостатки M-V-VM
- МВВМ-1: Общее обсуждение