Лекция 3. MVVM. CS193P Spring 2023.

Ниже представлен начальный фрагмент Лекции 3 Стэнфордского курса CS193P Весна 2023 «Разработка iOS приложений с помощью SwiftUI«.
Полный русскоязычный неавторизованный конспект Лекции 3 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны на платной основе.
Код находится на GitHub.

С полным перечнем Лекций и Домашних Заданий на русском языке можно познакомиться здесь.

В этой Лекции вы увидите, наверное, самое большое количество слайдов на протяжении всего семестра, но нам нужно изучить базовую архитектуру прежде, чем погрузиться в следующую часть демонстрационного приложения, которая будет освещаться во второй половине сегодняшней Лекции.
Есть вопросы, прежде чем я начну? Хорошо, нет.
Итак, Лекция номер 3.
У нас есть две очень большие темы для разговора: одна из них — MVVM, которая представляет собой архитектуру, парадигму дизайна, которую вы будете использовать для создания iOS приложений, вторая —  система ТИПов Swift:

Сегодня

MVVM

  • Парадигма конструирования

Система ТИПов Swift

  • structструктура
  • class — класс
  • protocol — протокол
  • “Don’t Care” ТИП (Generic) — “Не важно какой” ТИП (Дженерик)
  • enum — перечисления
  • functions — функции

Возвращаемся к Демо!

  • Применение MVVM к Memorize

В Swift есть разные ТИПы:  структура struct, протокол protocol и многое другое.
Итак, это две основные темы Лекции 3, а затем я вернусь к демонстрационному приложению, в которой мы начнем создавать логику игру Memorize, то есть что происходит, когда вы кликаете на карты.

Архитектура MVVM

Итак, что это за штука MVVM?
Надо сказать, что в Swift очень важно отделить логику и данные вашего приложения от пользовательского интерфейса (UI).
Это действительно очень важно.В некотором смысле, SwiftUI построен на той идее, что у вас будут ДАHHЫЕ и ЛОГИКА, которые связаны с тем, что делает ваше приложение, и еще у вас будет UI, который покажет это пользователю и будет взаимодействовать с пользователем как совершенно отдельная вещь. Так что это реально очень важно.
Та часть приложения, которая связана с ДАННЫМИ и ЛОГИКОЙ, например, в  нашем приложении Memorize это “что происходит, когда вы кликаете на карте?” или “карты лежат лицом вверх или лицом вниз?”, все это, мы называем Model (Моделью) нашего приложения. Вы услышите, что я использую это слово Model (Модель) сотни раз. Вся логика и данные “живут” в Model (Модели).

Всё, что мы делали до сих пор на этом курсе, был только UI и иногда мы будем называть его View, потому что это наш «взгляд», наш портал на Model (Модель).

Model и UI

Отделение «Логика и данных» от UI

  • SwiftUI очень серьезно относится к отделению логики и данных приложения от UI
  • Мы называем логику и данные нашей Model (Моделью)
  • Это может быть структура struct или SQL база данных или некоторый код машинного    обучения или многие другие вещи
  • Или любая комбинация таких вещей
  • UI — это “параметризованная” оболочка, которую “питает” и вызывает к “жизни” Model
  • Думайте о UI как о “визуальном” проявлении Model (Модели)
  • Model (Модель)  — это где “живут” такие вещи как isFaceUp и cardCount (a не @State в UI)
  • SwiftUI заботится о том, чтобы UI перестраивался всякий раз, когда Model (Модель) изменяется

Model (Модель) может быть одной структурой struct. Это может быть целая SQL база данных, полная всяких сущностей. Возможно, что это может быть какой-то код машинного обучения.
Это может быть  похоже на REST API для Интернета. Это может быть почти что угодно.
Наша Model (Модель) — концептуальная вещь, это не просто одна структура struct, хотя в нашем приложении Memorize это будет действительно одна структура struct, так как у нас очень простое маленькое стартовое приложение. Но я не хочу, чтобы вы думали, что ваша Model (Модель) не может быть более мощной и сложной.

Что касается UI части нашего приложения, то это на самом деле просто “параметризованная” оболочка, которую Model “кормит”. Одна из лучших фраз, которые я слышал о UI — это визуальное проявление Model, потому что Model — это то, чем реально является ваше приложение, это игра на запоминание Memorize, вот что это такое, так что вся эта логика игры находится в Model.
UI — это просто то, как вы показываете Model пользователю, то есть её визуальное проявление. Вам действительно хочется думать об этом именно так.
То, что мы разместили наши переменные isFaceUp и cardCount в @State, принадлежат Model. Всё это касается игры, в которую мы играем. Поэтому мы не хотим, чтобы они были в UI, и я постараюсь избавиться от них в UI, поместив их в нашу Model.

Одна из очень важных вещей, которые делает Swift, супер важная его обязанность, заключается в том, чтобы обеспечить отражение в UI любых изменений в Model. Model отображается в UI. И для этого у Swift есть огромная инфраструктура для этого. 
В свою очередь ВАМ НЕ нужно нести ответственность за то, что все, что есть в вашей Model, отображалось бы в UI. Swift всё это сделает за вас. Вам же просто нужно дать Swift несколько подсказок о том, что в Model влияет на ваш UI. Как только вы сделаете это единожды, дальше волноваться об этом не придется. 
Беспокоиться следует о том, чтобы разделить эти вещи: Model и UI.
Но если мы разделим Model и UI, как они будут между собой взаимодействовать? Потому что очевидно, что им нужно поговорить друг с другом.

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

Model и UI

Подключение Model к UI

Существует несколько вариантов подключения Model к UI …

  1. Редко, но Model может быть просто @State в View (это минимум разделения, его практически нет)
  2. Model может быть доступна только через “привратника” (gatekeeper) — класс classView Model” (полное разделение)
  3. Существует “View Model” класс class, но Model все еще доступна View напрямую (частичное разделение)

Почти всегда этот выбор зависит от сложности Model. . .

  • Model представляет собой SQL + struct(s) + что-то еще — скорее всего опция #2
  • Model представляет собой простейший кусок данных и не имеющая практически никакой логики — скорее всего опция #1
  • Что-то между опцией 1 и опцией 2 — опция #3
  • Сегодня мы будем говорить об опции #2 (полное отделение).
  • Мы называем такую архитектуру, которая подключает Model к UI таким образом, MVVM.
  • Model — View — ViewModel
  • Это основная архитектура для любого разумного по сложности SwiftUI приложения
  • Мы быстро взглянем на то, как использовать  #3 (частичное разделение) путем минимальных изменений MVVM.
  1. Редко, но один из способов, каким можно подключить Model к UI, заключается в том, что Model (Моделью) является @State в View. Я имею в виду, что если вы всю Model, всю логику в вашем приложении разместите в @State в вашем View, то, очевидно, View может получить к ней доступ. Это крайне минимальное разделение. Я бы, наверное, не стал этого делать.
  2. Давайте поговорим о том, что мы будем делать в ближайшее время. Номер 2 заключается в том, что Model доступна для UI только через “привратника” (Gatekeeper). У нас будет привратник (Gatekeeper), class ViewModel, и работа этого “парня” — поддерживать безопасную коммуникацию UI и Model. Это основной способ, который мы будем использовать в наших приложениях, и этот способ называется MVVM. Номер 2 мы будем использовать в 99% случаев при создании приложений
  3. Номер 3 является своего рода гибридом первых двух, то есть у нас есть этот привратник (gatekeeper), который мы называем ViewModel, но иногда у нас есть прямой доступ к Model. У нас будет public переменная var в Model, которая доступна UI и через неё UI на самом деле разговаривает с Model напрямую, прямо с игрой Memorize. По сути, это гибрид первых двух.

Итак, как узнать, какой из этих вариантов использовать? 

Ответ простой: всегда выбираем Номер 2.
Вот что я советую. Просто всегда выбирайте номер 2, особенно, если, как на предыдущем слайде, Model является SQL базой данных или всеми там указанными вариантами, в этом случае определенно нужен номер 2, потому что ViewModel находится посередине между Model и UI, это посредник, который знает, как использовать SQL и всё прочее. Вы же не хотите, чтобы ваш UI создавал SQL-запросы. Ваш UI просто поставляет информацию на экран, он должен быть максимально прост. Вот почему вы всегда будете использовать номер 2.

Но если у вас только очень, очень, очень простая Model, даже проще, чем наш Memorize, возможно, вы захотите применить @State решение, особенно если View совсем ненадолго появляется на экране. У такого View есть немного данных, что-то вроде данных изображения, которые он показывает, а потом оно уходит и уходит навсегда, тогда, возможно, вам не нужен этот дополнительный привратник (Gatekeeper) ViewModel

Что касается решения Номер 3, то это решение между 1 и 2 вариантами. Например, наша игра Memorize могла бы быть таким гибридным решением. Вы увидите это, когда мы построим нашу игру Memorize. Да, у нас есть этот посредник, этот привратник (Gatekeeper) ViewModel, но большую часть времени он просто пересылает запросы из UI напрямую Model (Модели), так что вы вполне могли бы создать аргумент в ViewModel, чтобы позволить UI видеть Model (Модель) напрямую. И при этом иметь привратника (Gatekeeper) ViewModel, чтобы вести какую-то другую “бухгалтерию”. 
Мне не нравится номер 3, потому что по мере роста вашего приложения вы начинаете создавать запутанный беспорядок, где ваш UI “разговаривает” и напрямую с вашей Model, и через этого “парня” — привратника (Gatekeeper).
Нет, это не гибкая система развития, так что, даже если вам, возможно, придется немного поработать с этим привратником (Gatekeeper), чтобы защитить вашу Model от вашего UI, это того стоит, особенно если учесть, что ваше приложение растет и становится более мощным. 

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

Мы поговорим о номере 2, об этом MVVM.
MVVM означает Model (Модель), View, ViewModel. Вот что такое M, V и
Model (Модель) — вы знаете, что это такое,
Ваш View – это другое слово для обозначения UI. 
И ViewModel — это привратник (Gatekeeper) посередине, о чем я вам говорил.

Интересное название ViewModel, потому что его задача — соединить View с Model и быть привратником (Gatekeeper) между ними. Мне нравится название ViewModel. Странное слово, но это то, что мы постараемся понять.

Итак, вот изображение MVVM

Вот моя Model. Вот мой View. И мы доберемся до вещи посередине через секунду. 

Model

Давайте поговорим о месте Model (Модели) в архитектуре в целом. 
Model является UI независимой. Мы даже не собираемся импортировать SwiftUI в нашу Model. Она действительно не зависит от UI. В нашем приложении Memorize мы будем играть в игру Memorize  с картами, на которых может быть что угодно: изображение в формате JPEG, эмодзи и вообще любое.
Это Generic (обобщенная) игра Memorize без UI, Model часть приложения, которая абсолютно независима от UI, это очень важно понимать. 

Как я уже говорил, Model — это данные Data вашего приложения, например, лежат ли карты «лицевой» стороной вверх или вниз, и логика Logic, например, что происходит, когда выбирается карта, какие карты переворачиваются и прочее.
Model — это обе эти вещи вместе взятые. 
Важно понимать, что Model — единственный источник ИСТИНЫ (“Truth”) для данных Data и логики Logic, так что, если вы хотите узнать какие-то данные Data или вы хотите выполнить определенные логические действия, вам нужно поговорить с Model.
Мы НИКОГДА не будем хранить эти данные Data где-то ещё, кроме Model. Например, мы НИКОГДА не будем копировать эти данные Data в ViewModel, которую собираюсь вам показать и, конечно, НЕ в @State в вашем UI. НИКОГДА не делай этого. Если вы хотите получить информацию, то возвращаетесь к Model.
Именно Model — единственный источник ИСТИНЫ (“Truth”).
О том, что знает Model, вы всегда должны спрашивать только Model.

View

Теперь поговорим о View. Что такое View?
View, как я уже сказал, это визуализация Model. View всегда должен выглядеть так, как выглядит Model. Что бы ни происходило в Model, вы должны увидеть это на экране.

По этой причине View не имеет состояния. У View НЕТ НИКАКОГО СОСТОЯНИЯ (Stateless). View просто всегда показывает то, что есть в Model, ему просто не нужно никакое состояние.
Вот почему любое состояние, которое у нас есть в View, мы отмечаем @State, чтобы понять: “О, у нас есть состояние, это нехорошо, View должно быть Stateless”. 
Это нормально иметь @State в очень редких обстоятельствах, но это необычно, и это специально помечается таким образом, чтобы мы знали, что мы делаем что-то действительно редкое.
Views почти никогда не имеют состояний, они постоянно показывают то, что находится в Model.
Вы, наверное, уже заметили, что ваш View является декларативным (Declared). Мы не пишем код для нашего View в императивной форме.
Вы, ребята, знаете, что означает слово “императивный”? 
Это когда вы пишете код, который вызывает функцию, затем вы вызываете другую функцию, чтобы заставить что-то произойти, а затем вы вызовете ещё одну функцию, которую вы пишите императивно. 

Вы, наверное, заметили, что у нашего View есть только переменная var body, в которой мы просто перечисляем Views, которые входят в наш UI. Мы их модифицировали с помощью View модификаторов, но мы их просто перечисляем, они сидят там. Это VStack, внутри которого карты cards и кнопки Button. Мы просто рассказываем, декларируем, что собой представляет наш UI, а затем данные Model управляют всем этим.
Любая часть нашего UI, которая может меняться, меняется, потому что наша Model меняется и вызывает эти изменения.
Итак мы декларируем UI. Так что это декларативный UI. 
И в результате этого мы приходим к слову, которое вы будете видеть многократно, «reactive» (реактивный), потому что UI реагирует (react) на все изменения в Model.

Изменяется Model — обновляется UI, потому что он должен это делать, потому что он всегда показывает, что находится в Model.

ViewModel

Теперь мы можем поговорить об этом MVVM
И это работает так…

Мы собираемся ввести еще одного актера, ViewModel, который будет “привязывать” (bind) View к Model. Задача ViewModel состоит в том, чтобы соединить эти две вещи друг с другом.

Единственная линия на рисунки сообщает: “data flows this way read only” (“данные текут в этом направлении и они только для чтения”).  Верно, потому что этот UI не имеет состояний (Stateless) и его подпитывает Model. ViewModel собирается вмешаться в эту линию, и ViewModel не только “привязывает” (bind) View к Model, но также может служить своего рода переводчиком (Interpreter) между Model и View. Например, если Model является SQL база данных, которая не делает SQL запросы, то ViewModel может выполнять SQL запросы, превращать их результаты в обычные переменные var или во что-то еще, чтобы View мог их видеть, то есть ViewModel выполняет интерпретацию Model.
ViewModel также является привратником (Gatekeeper) между этими двумя ребятами: Model и View. Он защищает Model, чтобы View никогда не смог сделать ей ничего плохого. И я собираюсь показать вам основной способ, как это делается, через минуту. 

Итак, ViewModel — это привратник (Gatekeeper), переводчик (Interpreter), это то, что контролирует поток данных между этими двумя вещами, Model и View. В общем, эта линия проходит вверх и вниз. Данные передаются от Model через ViewModel в View.

Как это все работает? 
Если что-то происходит в  Model, задача ViewModel— замечать эти изменения. 

Итак, опять же, если Model — это SQL база данных, то можно подписаться на получение уведомления, когда что-то там меняется.
Если Model — это Swift структура struct, то это вообще фантастика, потому что Swift может автоматически отслеживать изменения в структуре struct и сообщать вам, когда что-то в структуре struct меняется. Вы поймете, почему это так, когда мы поговорим о система ТИПов в Swift.

Что бы это ни было, ViewModel должна уметь замечать изменения. Это фундаментально.
Теперь, когда ViewModel замечает изменения, он немедленно публикует всему Миру сообщение : “Что-то изменилось в Model…” («something changed in the Model«). И любой желающий может прослушивать эти сообщения.

Как только Model сказала: “Что-то изменилось…”, включается SwiftUI. Он смотрит на свой View и говорит: «Ой!, что-то изменилось, для меня это означает перерисовывать View”. Он очень сообразительный по отношению к перерисовке. Он будет перерисовывать только те Views, которые на самом деле были затронуты этим изменением, он делает это за вас, так что вам не придется об этом беспокоиться. Вам не нужно ничего делать, чтобы это произошло.

Итак, вот какой у нас процесс: 

  • заметить изменения,
  • опубликовать, что происходят изменения,
  • Swift автоматически определяет, какие Views необходимо перерисовать в связи с обнаруженными изменениями, и перерисовывает их.

Откуда он это знает? Как он это понимает?
Опять же, все дело в функциональном программировании, в том, что структуры struct в Swift являются VALUE ТИПами, мы можем очень легко обнаружить, когда они меняются, это встроено в сами структуры struct в Swift. Именно Swift позволяет этому происходить.
Я не буду говорить об этом механизме прямо здесь и сейчас, но это очень круто.
Я просто разместил на слайде эти вещи: View модификаторы и эти небольшие @вещи, такие как @State. Они все разные и я разместил их здесь для будущих ссылок.

Я не собираюсь говорить о любом из них прямо сейчас, но позже в этом семестре, когда вы увидите, как я говорю об одной из этих вещей, вы возможно вспомните и скажите: “О, я пойду взгляну на этот MVVM слайд. И смотри-ка, да, эта вещь связана с MVVM”.
Мы будем говорить, например, об @ObservedObject, ObservableObject и @Published где-то на следующей Лекции, a об остальных чуть позже в этом семестре.
Итак, это основы MVVM в направлении от Model к View.

Но есть большой вопрос: а как насчет другого направления? 
Ведь в View вы можете кликнуть (Tap) на чем-то, вы можете выполнить жест Swipe и делать другие вещи. Пользователь может взаимодействовать с экраном, и это может повлиять на Model.
Так как же нам идти этим путем? Потому что представленная на слайде стрелка указывает только одно направление и все происходит только в этом направлении — от Model к View.
Ну, это делается путем обработки «User Intent» (“Намерения пользователя”), и это еще одна вещь, которую выполняет ViewModel.
Когда кто-то кликает (Tap) на чём-то в View, он вызывает функцию в ViewModel, сказав: “У пользователя есть следующее Намерение (Intent)”.

Например, в нашей игре Memorize пользователь хочет выбрать эту карту. У вас не будет метода с именем “tap” в View. Это не имело бы смысла. Это Ui вещь — Tap. У вас будет метод в вашей ViewModel с именем «choose this card» (“выберите эту карту”).
Понимаете? Это Намерение (Intent) пользователя. Пользователь намеревается сделать что-то семантически значимое для Model. Еще раз, задача ViewModel состоит в том, чтобы превратить это Намерение (Intent) в кучу SQL запросов или что-то в этом роде, когда имеет смысл выразить это Намерение во что-то подходящее для Model
Так что да, очень важна эта идея Намерения (intent). 
Иногда Намерение (intent) типа “выбери карту”, приводит к тому, что у нашей Model игры Memorize будет метод выбора карты, который фундаментально определяет игру Memorize, так что это будет выглядеть так, как будто наш ViewModel почти не работает, ничего не делает, a просто пересылает сообщения. Но ViewModel все еще остается привратником (Gatekeeper), он всё ещё связывает их вместе, просто наше первое приложение действительно очень простое. Но во втором приложении, которое мы создадим, вы увидите, что есть Намерения (intents), которые не могут быть сопоставлены непосредственно с одним вызовом в Model. Во всяком случае, вот что здесь происходит — Намерение (intent).

Что же происходит, когда это Намерение (intent) вызывается в ViewModel
Да, конечно, ViewModel изменяет Model каким-то способом, чтобы выразить любые намерения  (intents) пользователя.

Затем происходит то, что произошло раньше и происходит снова: Model изменилась, ViewModel замечает изменения, он выполняет все эти публикации “что-то изменилось …”, View делает свое дело и обновляется. 
Происходят в точности то же самое, что происходило раньше. 

Поэтому, когда мы идем по этому пути (от View к Model), единственное, что важно – это Намерение  (Intent). Как только вы вызываете Намерение (intent), ViewModel обновляет Model, а затем происходит обычное обновление. Наш Stateless (без сохранения состояния) UI отражает новое состояние Model после Намерения (Intent), которое мы выразили.

И это все. Это весь MVVM.

Система ТИПов Swift

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

Архитектура

Варианты ТИПов

  • struct структура 
  • class — класс
  • protocol — протокол (часть 1)
  • “Don’t Care” ТИП (Generic) — “Не важно какой” ТИП 
  • enum — перечисления
  • functions — функции

Я собираюсь рассказать о протоколах protocol лишь половину информации, я бы сказал “Протоколы protocol. Часть 1”. Ещё о протоколах protocol мы поговорим на Лекции 5.
Я пропущу перечисления enum не потому, что перечисления enum не так важны, перечисления enum – это важный ТИП, но я просто не успею рассказать обо всех ТИПах.
Мне нужно поговорить ещё об этом последнем ТИПе — функциях functions.
И да, я говорю здесь о ТИПах: функции functions — это ТИПы.

. . . . . . . . . . . .

Это небольшой начальный фрагмент Лекции 3 …
Далее на Лекции 3 рассматриваются следующие вопросы:

  • ТИПы struct и class
  • Инициализаторы (initializers)
  • Различие между struct и class
  • Generics
  • Протоколы protocol
  • Функции как ТИПы 
  • Замыкание Closure
  • Возвращение к демонстрационному примеру
  • Model
  • ViewModel

Полный русскоязычный неавторизованный конспект Лекции 3 в формате Google Doc и в виде PDF-файла, который можно скачать и использовать offline, доступны на платной основе.
Код находится на GitHub.

С полным перечнем Лекций и Домашних Заданий можно познакомиться здесь.