За последние несколько лет шаблон проектирования Модель-Представление-Модель представления (MVVM) стал популярным в архитектуре приложений iOS, борясь с проблемой массивного контроллера представления, которую нам оставила традиционная архитектура Модель-Представление-Контроллер (MVC). с участием. При помощи четвертого компонента, модели представления, MVVM предлагает средства для нарушения логики представления в контроллерах.

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

Модель просмотра: простое * определение

Мы широко определяем «модель представления» как тип, который инкапсулирует данные, необходимые для заполнения определенного вида представления. Если говорить более кратко, модель представления - это модель представления. Эти основные определения оставляют желать лучшего. Наши модели представления просто хранят данные и не выполняют никаких операций? Нет - мы разрешаем им выполнять операции только для отображения данных, поэтому давайте расширим это определение до типа, который инкапсулирует данные, необходимые для заполнения определенного вида представления, и логику представления, необходимую для преобразования данных в свойства. которые можно отобразить. Хорошо, но что квалифицируется как «логика представления» и что в данном контексте означает «преобразование»? 😵

Просмотр модели: конкретный пример

Давайте начнем сначала и обсудим модели просмотра с точки зрения UIKit и архитектуры приложения iOS на примере. В конечном итоге мы собираемся создать ячейку, которая может отображать комментарий к сообщению в блоге, для интерфейса, который выглядит следующим образом:

Начнем с представления. В частности, UITableViewCell, который является подклассом UIView. UIView определяет прямоугольник содержания и содержит некоторые сведения о пользовательском вводе, с которыми мы поговорим позже. Но эй, это визуально! Как это выглядит?

Отлично, у нас есть вид. Наша ячейка пуста и отображается со значением по умолчанию backgroundColor. Мы еще не предоставили ему никаких данных и не предоставили никаких средств для предоставления данных. Давайте продолжим нашу игру и заполним представление компонентами пользовательского интерфейса. Чтобы наши фрагменты кода были сосредоточены на обсуждаемой теме, мы оставим макет для Interface Builder, но вы можете настроить и расположить представление, как вам нравится. Вот окончательный вид нашей ячейки:

Отлично, у нас есть контент! Или… заполнители для содержания. Нам нужно будет перенести данные из уровня модели нашего приложения в копии этой ячейки для отображения на экране. Прежде чем наша модель представления обретет смысл, нам нужно определить этот уровень приложения.

Модель

Давайте посмотрим на простой тип модели, который отражает объект, возвращаемый веб-службой. Предположим, что в приложении все это используется, несмотря на то, что ячейка в нашем примере требует только подмножества.

Для удобства вот тип CommentAuthor, упомянутый в Comment выше.

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

  • Имя автора
  • Дата публикации комментария
  • Текст комментария
  • Редактировал ли пользователь текст комментария с момента его публикации.

Хотя использование типа значения для моделирования Comment избавит нас от возможности манипулирования представлением любым аспектом уровня модели приложения, мы по-прежнему собираемся предоставлять представлению только те данные, которые ему необходимы. В частности, наш подкласс ячейки не будет знать о типе Comment. Мы можем сделать это разными способами, в том числе:

  1. Предоставление компонентов пользовательского интерфейса в нашем подклассе ячеек для заполнения данными из модели.
  2. Сохранение конфиденциальности компонентов пользовательского интерфейса и предоставление индивидуальных свойств для каждой части данных, которые будут заполнять компоненты пользовательского интерфейса при установке.
  3. Предоставьте единую точку конфигурации: модель представления

Угадай, какой я выберу.

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

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

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

Модель просмотра и просмотра

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

Мы объявили два типа: CommentCell, который является нашим UITableViewCell подклассом, и CommentCell.ViewModel, модель представления, которая поддерживает этот тип ячеек. Пока что у нас есть одно свойство, объявленное на CommentCell, которое является экземпляром CommentCell.ViewModel. В конечном итоге это станет единственной точкой настройки.

Обратите внимание, что в типе модели представления большинство свойств можно легко сопоставить с нашей моделью позже. Мы не хотим, чтобы уровень контроллера представления брал на себя ответственность за определение того, как форматировать и отображать имя автора. Мы также помещаем имя автора в тот же ярлык, что и разграничение «Отредактировано», чтобы оно могло быть естественным. Этот макет специфичен для нашего типа ячейки, и мы хотим разместить эту логику в нашей модели представления. Это пример логики представления, о котором мы упоминали ранее. Точно так же контроллер представления не должен отвечать за определение того, как форматировать дату, отображаемую в нашем представлении. Скорее, мы оставляем эту логику представления нашей модели представления. Мы поместим эту логику представления в расширение типа модели представления, чтобы отделить его от чисто модельных свойств.

Если скрыть эту логику представления глубоко внутри подкласса aUIViewController, тестирование будет сложнее. Мы склонны оставлять эти вычисленные свойства открытыми в наших моделях представления, чтобы к ним было легче получить доступ в нашей тестовой цели, без необходимости создавать экземпляры каких-либо UIViewControllers или UIViews.

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

Единая точка настройки

До сих пор мы определили только одно свойство в нашем подклассе ячейки.

var viewModel: ViewModel?

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

Мы определяем единый способ настройки нашего представления. Это делает конфигурацию более безопасной, поскольку становится невозможным указать только подмножество открытых свойств, полагаясь только на prepareForReuse() для сброса свойств, которые можно оставить неустановленными. Скорее, мы заставляем наш контроллер установить их все сразу. Ячейка нашего примера относительно проста, но в более сложных случаях порядок, в котором обновляются компоненты пользовательского интерфейса, может иметь большее значение, и, раскрывая только одно свойство, мы можем гарантировать согласованность и предсказуемость порядка обновлений.

При таком подходе наша UITableViewDataSource реализация будет относительно простой. Нам просто перевести на тип модели представления, представленный нашей реальной моделью на этом уровне.

Мы взяли несколько свойств из нашего объекта модели, и на этом все. Наши модели представления содержат только данные, необходимые для заполнения компонентов пользовательского интерфейса. Они также указывают средства преобразования этих данных во что-то для отображения на экране, как мы это делали в нашем расширении CommentCell.ViewModel.

Если вы внимательно следите, то заметите, что наша модель представления определяет его authorImage как UIImage?, а не URL, поэтому перевод из нашего фактического уровня модели для этого конкретного свойства не так просто. Вы заметите, что в приведенном выше фрагменте мы просто передаем nil вместо authorImage. 🤔 Кроме того, что происходит, когда вы нажимаете кнопку ответа на ячейке? Ну пока ничего.

Модели просмотра - не серебряная пуля против массивных контроллеров обзора!

Мы разместили логику представления в месте, удаленном от контроллера представления. Однако мы сохранили обязанности нашей модели представления простыми и четко определенными.

Давайте поговорим о слоне в записи блога. Откуда берутся эти изображения авторов комментариев?

Извлечение данных

Наша модель представления имеет свойство UIImage?, а не URL. Мы делаем это, потому что не хотим, чтобы наша модель представления отвечала за загрузку изображений. Модель представления - это модель представления (помните?), И вы бы не стали помещать сетевой код в свой традиционный слой модели, не так ли? А вы бы?

(Хорошо, может быть, вы уже участвовали в этом одном проекте в начале своей карьеры, но в конечном итоге он вас укусил, не так ли? И вы действительно надеетесь, что никто никогда не увидит этот объект модели, который извлекает свои данные из сети в основном потоке, анализирует их и настаивает, правда?)

Так почему же наша модель представления отвечает за получение изображений из сети? Или с диска? Мы не думаем, что так должно быть.

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

Поэтому вместо того, чтобы устанавливать для нашего authorImage значение nil, мы могли бы UITableViewDataSource попросить уровень сохраняемости нашего приложения получить изображение для определенного URL. Это изменит нашу конфигурацию сверху на что-то вроде следующего.

Метод image(for:) в этом примере вернет необязательный UIImage, поскольку нет гарантии, что у нас действительно есть это изображение. Мы бы отложили загрузку изображения, отдавая приоритет отображению данных, которые у нас уже есть. Мы могли бы начать загрузку изображений в tableView(_:willDisplay:forRowAt:) в тех случаях, когда их нет на диске, или, возможно, когда срок действия нашего локального кеша истек, и мы хотим обновить существующее изображение, уже сохраненное на диске. Когда у нас есть это изображение, скажем, в обработчике завершения метода сетевого запроса, мы можем просто установить его в модели представления ячейки следующим образом.

cell.viewModel?.authorImage = imageFromNetwork

Теперь вы могли заметить, что мы оставили свойство authorImage на CommentCell.ViewModel a var, а не на let. Это замечательно, потому что позволяет нам изменять значение отображаемого свойства, которое мы ожидаем изменить, но его установка по-прежнему запускает viewModel свойство didSet, избавляя нас от любых проблем, связанных с порядком, в котором настраиваются компоненты пользовательского интерфейса. Несмотря на то, что мы обновляем одно свойство, мы все же достигли единой точки настройки. (Должен ли я создать сниппет TextExpander для этой фразы?)

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

Делегирование действий

Обычно обработка касания ячейки может выполняться либо с помощью перехода к раскадровке, либо с помощью UITableViewDelegate метода tableView(_:didSelectRowAt:), обычно реализуемого типом UIViewController или, который используется UIViewController. Но обратите внимание, что помимо возможности нажать на ячейку, у нас также есть кнопка ответа.

Итак, принимает ли участие в этом взаимодействии модель представления? Нет. Нам нравится следовать той же схеме, что и при выборе ячеек, в том смысле, что это можно сделать с помощью перехода раскадровки, если мы указали наш макет в ячейках прототипа в раскадровке, или разрешив UIViewController обрабатывать взаимодействие. Мы не хотим, чтобы модель представления или представление брали на себя какую-либо из этих обязанностей. Давайте рассмотрим подход без раскадровки.

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

Независимо от того, используете ли вы @IBAction или регистрируете цель / действие с помощью addTarget(_:action:for:), вам просто нужно будет вызывать закрытие при нажатии кнопки.

Все, что нам осталось, чтобы выполнить действие по нажатию кнопки ответа CommentCell, - это установить replyButtonTapHandler. Мы делаем это в том же месте, где мы настроили свойство viewModel, о котором говорилось ранее.

Итак, единственная точка настройки - это все данные, относящиеся к дисплею. Мы разделяем действия на их собственные свойства, так как модель представления не отвечает за обработку пользовательского ввода. Эти действия также можно обрабатывать через делегирование, но мы предпочитаем закрывающие свойства для этого типа взаимодействия.

Будьте (View) Образцовым гражданином

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

Чтобы узнать больше о моделях просмотра, наши друзья Chris Dzombak и Soroush Khanlou провели эпизоды 2 и 3 своего подкаста Fatal Error на эту тему, которые включают качественное освещение проблем, которые они могут решить, MVVM шаблон проектирования, связанный с разработкой для iOS, подводными камнями и т. д. Ознакомьтесь с примечаниями к шоу для этих эпизодов, которые включают ссылки на многие другие соответствующие ресурсы. См. Также выступление Энди Матущака на NSSpain в 2015 году Давай поиграем: рефакторинг мегаконтроллера.

Что дальше?

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