Лекция 2 CS193P Spring 2020 — MVVM и система ТИПов в Swift. Часть 2.

На сайте представлен полный неавторизованный хронометрированный конспект на русском языке Лекции 2 Стэнфордского курса CS193P Spring 2020 “Разработка iOS с помощью SwiftUI ”.

Первая часть — 0 — 38 минута находится здесь,
Вторая часть — 38 — 104 минута находится в этом посте.

Код находится на GitHub.

——————      ПРОДОЛЖЕНИЕ КОНСПЕКТА      —————————

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

——- 38-ая минута лекции ———

Как мы будем это делать?
До сих пор мы работали над кодом, который представлен на экране, и это View.
Если следовать логике MVVM, то мы работали над первым V, которое представляем View, и следующим куском, которому мы уделим внимание, будет Model, то есть первое M.
Model является UI независимой, то есть она ничего не знает о том, как игра будет показана на экране пользователю.

Для Model нам нужно добавить новый Swift файл в Xcode, и мы делаем это с помощью меню File->New->File:

Вы видите здесь множество различных файлов, которые можно создать, но реально это сводится лишь к двум из них.
Мы можем создать новый SwiftUI View. Это что-то наподобие struct Whatever: View c переменной var body и со всем прочим:

Другой файл создает НЕ-UI пустой Swift файл, который нам и нужен в данный момент, потому что Model — это НЕ-UI структура struct:

Давайте дважды кликнем на нем.
Нас спрашивают, где мы хотим запомнить этот файл и как мы хотим его назвать.
Этот файл будем “сердцем” нашей игры, так что я дам ему имя MemoryGame, это будет структура struct MemoryGame:

В нижней части этого экрана нас спрашивают, где мы хотим разместить этот файл, и здесь ЖЕЛТЫМ цветом помечена папка, в которой будет размещен файл, Это та самая папка, которая имеет ЖЕЛТЫЙ цвет в списке слева.
Но есть еще и ГОЛУБАЯ папка, которая также соответствует ГОЛУБОЙ папке слева:

Мы НИКОГДА НЕ должны ничего размещать в ГОЛУБОЙ папке.
Мы должны размещать файлы в папках, окрашенных в ЖЕЛТЫЙ цвет:

На самом деле не имеет значения какую группу вы выберете здесь, вы лишь должны убедиться, что размещаете файл в том же месте, где размещен файл ContentView. Если вы видите ContentView, то вы выбрали правильное место.
Давайте создадим этот файл и кликнем на кнопке “Create”.
Итак, это MemoryGame:

Как видите, нет строки с import SwiftUI, так как это НЕ-UI компонент.
Но зато есть import Foundation, об этом фреймворке я говорил в прошлый раз. Он содержит массив Array, словарь Dictionary, строку String, целое значение Int, булевское значение Bool и другие базовые ТИПы.
Но в нем нет View, Text, RoundedRectangle и других UI элементов.
Мы создаем структуру struct. Помните? Я говорил вам, что структура struct — это предпочтительная структура данных. Я назову мою структуру MemoryGame, но у неё НЕ будет :View, так как она не будет вести себя как View и вообще это НЕ-UI
вещь:

Я создал структуру struct, которая представляет мою Model, но, между прочим, моей Model необязательно может быть struct, это может быть SQL база данных или интернет HTTP запрос, с помощью которого я получаю информацию, и всё-таки в большинстве случаев это структура struct.
Но может быть также и класс class. Вполне возможно при определенных обстоятельствах иметь Models, которые являются классами class, но если у вас нет никаких обстоятельств, то вы начинаете с предпочтительной структуры данных, а именно со структуры struct.
Когда я создаю Model, я всегда спрашиваю себя, а что эта Model делает?
И это позволяет мне разместить в Model переменные vars и функции, которые действительно описывают то, что делает Model.
Когда я думаю о MemoryGame, карточной игре “на совпадение”, то я думаю, что она должна иметь карты. Поэтому у игры MemoryGame должна быть переменная var cards,  и, конечно, все переменные  vars должны иметь ТИП. ТИП нашей переменной var cards — это массив Array, а массив, как мы знаем, это generic ТИП, то есть “Не важно, какой” ТИП, но в случае массива Array“Не важно, какой” — это ТИП тех элементов, которые содержатся в массиве Array.

——- 40-ая минута лекции ———
При декларировании карт cards, мне нужно указать реальный ТИП элементов в этом массиве Array, и немного забегая вперед, я дам имя Card этому реальному ТИПу элементов массива Array и прямо тут же в коде я определяю Card как структуру struct Card:

Структура struct Card представляет единственную карту.
Заметьте, что я разместил структуру struct Card внутри моей игры struct MemoryGame, так что полное имя структуры, соответствующей единственной карте, будет MemoryGame.Card. Структуры, вложенные в другие структуры, именно так и именуются, так что мы можем точно сказать, что это не просто какая-то игральная карта или какая-то случайная карта, а именно карта, принадлежащая игре MemoryGame. Именно поэтому мы так и поступили. По ходу дела мы обнаружим еще ряд незначительный преимуществ вложенных структур.
Что ещё необходимо моей игре MemoryGame помимо ряда карт?
Мне необходим способ выбора карты Card.
И сейчас впервые вы увидите определение Swift функции. И, конечно, оно начинается с ключевого func, затем следует имя функции choose и далее любые аргументы. В нашем случае это единственный аргумент — карта card:

Заметьте, как и было обещано, почти все аргументы функции имеют метку, и это делает понятным вызов этих функций. Например, если я вызываю choose, то становится понятным, что я выбираю карту, и метка card аргумента тут же “под рукой”.
Внутри этой функции choose мы напишем логику нашей карточной игры “на совпадение карт”.
Но в данный момент я просто использую предложение print. Функция print — это прекрасная Swift функция, которая печатает строку String.
Например, сейчас я печатаю пустую строку:

Но я могу распечатать текст “card chosen: ” и затем разместить где-то карту card в этом предложении print. В других языках программирования я должен был бы использовать %s и только затем разместить card :

Но в Swift мы не делаем этого. Когда мы хотим вставить что-то в строку String, что имеет другой ТИП, то мы делаем это с помощью “обратного слэша” и круглых скобок \( ), в которых размещаем то, что имеет другой ТИП:

И если \(card) можно превратить в строку String, то это будет работать. Надо сказать, что Swift большой мастер ВСЁ превращать в строку String.
В данный момент у структуры struct Card нет никаких переменных vars, так что предложение print, возможно, не выполнит свою работу или распечатает пустую структуру struct Card, но, очевидно, мы собираемся добавить туда переменные vars, и тогда предложение 

      print («card chosen:  \(card)«)

… будет распечатывать значения этих переменных, преобразованных в строку String.
Это супер мощный механизм —  \(card). Я призываю вас его использовать, он отлично подходит для отладки. Он может печатать любые вещи, если что-то происходит. И это замечательно.
Это достаточно простая функция. По мере прохождения этого курса мы будем изучать различные кусочки синтаксиса для функций. Например, если функция возвращает какое-то значение, то для этого используется маленькая стрелочка, которая как бы говорит о том, что “выходит” из функции, а затем ТИП возвращаемого значения, например, строка String:

Но наша функция choose ничего не возвращает:

Функция может иметь ещё один аргумент, ТИП которого, например, Int или ещё что-то:

Функция может иметь сколько угодно аргументов.
Итак, это фактически и есть вся наша игра MemoryGame:

У игры MemoryGame есть карты cards, и вы можете их выбирать с помощью функции choose.
Но нам нужно решить, как карта Card выглядит и какую важную информацию о ней мы должны знать.
Одна вещь, которую мы уже знаем о карте Card — это то, что она может лежать “лицевой” стороной вверх или вниз и это определяется переменной var isFaceUp: Bool:

Я думаю, что мне также необходимо знать, совпала ли карта Card или нет. И это тоже будет булевская переменная var isMatched: Bool:

Что ещё необходимо для карты Card? Я думаю необходимо содержимое content карты Card, то есть то, что находится на карте.
Это переменная var content:

Но вопрос в том, какого ТИПа переменная var content?
Я могу представить себе создание карточный игры с изображениями Image:

Конечно, мы можем создать карточную игру с эмоджи, которые являются строками String:

Можно создать карточную игру со словами Word или с числами Int:

В данном случае у нас переменная content имеет ТИП Int, а может быть и String, или Image, или что-то ещё?
Похоже на то, что нам собственно “Не важно, какой” ТИП у переменной var content.

——- 45-ая минута лекции ———
Если мы находимся внутри MemoryGame, то вы можете разместить что угодно на картах Card.
Мы создаем UI независимую логику игры, так что нам действительно НЕ ВАЖНО, что находится на картах Card.
Так что  переменная content имеет “Не важно, какой” ТИП, и я назову его CardContent:

Это “Не важно, какой” ТИП, и меня просят вверху в угловых скобках < > СООБЩИТЬ МИРУ, что CardContent— это generic ТИП:

Если вы хотите использовать игру MemoryGame, то вам нужно точно сказать, какой реальный ТИП заменит CardContent.
Как только мы начнем использовать в нашей игре с эмоджи эту Model, мы должны написать MemoryGame<String>, потому что эмоджи — это просто символы в строке String.
Это действительно удивительно простой пример того, как можно иметь дело с “Не важно, какой” ТИПами. На самом деле так и есть — MemoryGame совершенно не беспокоится о том, что находится на картах cards.
Итак, у нас есть эта Model и у нас также есть View :

Давайте реализуем 3-ю часть архитектуры MVVM, а именно ViewModel.
ViewModel будет своего рода “клеем”, который “склеивает” полностью UI НЕзависимую вещь с полностью UI зависимой вещью.
Мы опять создадим новый файл с помощью меню File -> New -> File:

И это НЕ будет SwiftUI View, хотя, конечно, ViewModel — UI зависимая вещь, но это НЕ реальное View, а ViewModel. Это будет обычный Swift File:

Я назову мою ViewModel здесь вверху EmojiMemoryGame, потому что это специфика этой игры, которая состоит в использовании эмоджи в качестве того, что рисуется на картах.
Кроме того, я должен убедиться, что выбрал правильную папку для размещения, то есть там, где разместились остальные мои файлы:

Прекрасно. Это EmojiMemoryGame. И у нас импортируется Foundation:

На самом деле я мог бы импортировать SwiftUI, если бы захотел: 

Хотя я вовсе не собираюсь создавать здесь UI, так как UI создается в моем View.
Но по сути ViewModel — это всё таки UI вещь, потому что она ЗНАЕТ, как рисовать на экране.
Фактически, это основная цель в её “жизни”:  взять UI НЕзависимую Model с именем MemoryGame и транслировать её так, чтобы она отображалась на экране каким-то образом.
Прежде чем мы погрузимся в создание EmojiMemoryGame, моей ViewModel, давайте скроем Preview. И мы можем это сделать с помощью маленькой кнопочки в правом верхнем углу экрана и выбора пункта меню “Show Editor Only”:

В результате Preview скроется. Для того, чтобы оно появилось вновь, мы должны выбрать пункт меню “Canvas” для той же кнопки:

В результате Preview появится вновь, но опять его скроется с помощью всё той же кнопки.
Давайте создавать здесь нашу ViewModel.
Но прямо с места в карьер замечу, что я собираюсь сделать мою классом class. Это будет класс class с именем EmojiMemoryGame. Между прочим, класс class — это объектно-ориентированное программирование, и я мог бы указать некоторый superclass:

Но у меня нет никакого superclass:

Через мгновение я собираюсь объяснить, почему я использую класс class вместо структуры struct.
Но давайте сначала подумаем, что собой представляет ViewModel.
Вы знаете, что по сути, это портал (главный вход) между Views и нашей моделью Model. Это ДВЕРЬ, у которой ожидают Views, чтобы добраться до Model.
Так что точно, в чем ViewModel нуждается, это своего рода переменная var, через которую может быть обеспечен доступ к Model. Я назову эту переменную var model:

Возможно, вам не следует никакую переменную называть так, как назвал её я — var model, потому что “model” — это “концепция” , а не что-то конкретное и семантическое , но я назвал её model чисто из обучающих соображений. Реально вам следовало бы назвать эту переменную чем-то наподобие game, чем-то более информативным относительно того, что это такое. Это игра на совпадение, так что, возможно её стоит назвать game, или memoryGame, или ещё как-то.
Но я назвал эту переменную model, так что если вы будете просматривать этот код, то подумаете: “О! Этот код обеспечивает доступ к Model.”
Какой же ТИП имеет наша переменная var model?
Давайте обратимся к только что созданной Model:

Это структура struct MemoryGame, это Generic struct MemoryGame с “Не важно, какой” ТИПом CardContent, который представляет собой содержимое карты Card и в нашей эмоджи игре содержимым карты, конечно, являются строки String. Эмоджи — это строки String.
Так что ТИП переменной model будет просто MemoryGame и заменяя Generic ТИП CardContent на реальный String, получаем MemoryGame <String>:

Вот так просто.
Теперь поговорим, почему EmojiMemoryGame — это класс class и, возможно, я даже смогу создать аналогию между ViewModel и Model, которая поможет нам понять, как они взаимодействуют друг с другом.

——- 50-ая минута лекции ———
Самым большим преимуществом класса class является возможность поделиться (share) указателем (pointer) на него со всеми желающими, потому что класс class “живет” в “куче”, и у нас есть указатели (pointer) на него. Это как раз то, к чему мы привыкли в объектно-ориентированном программировании.
Так как класс class “живет” в “куче”, и у вас могут быть указатели (pointer) на него, то все наши Views могли бы иметь указатели на него, и если мы начинаем создавать сложный пользовательский интерфейс (UI), состоящий из множества Views, то всё это множество Views, возможно, захочет посмотреть на Model через этот портал (входные ворота) — ViewModel.
Вот что собой представляет ViewModel — это портал к модели Model. Мы хотели бы смотреть”через” него на нашу Model. Все Views хотят это делать и поэтому они все хотят иметь указатель (pointer) на этот портал.
Это действительно отличное использование класса class для того, чтобы все эти Views могли смотреть на Model через этот единственный портал, и у каждого из них был бы указатель (pointer) на этот единственный портал ViewModel.
Но как и со многими другими вещами, самая сильная сторона класса class одновременно может быть и его самой слабой стороной.
Проблема заключается в слишком большом количестве различных людей, имеющих указатели (pointers) на одну и ту же ViewModel, так что если любой из них что-то испортит, то “вечеринка” закончится для всех.  Особенно при таких обстоятельствах.
Вот моя аналогия.
Представьте, что у вас есть ДОМ.
Внутри вашего ДОМА “живут” множество Views, а наша ViewModel, которая в данном случае является классом class EmojiMemoryGame, является ВХОДНОЙ ДВЕРЬЮ, потому что по сути ViewModel — это дверной проем, портал для всех Views чтобы выйти на Model.
Model — это ВНЕШНИЙ МИР. Всё, что находится за пределами дома, — это Model.
Все наши Views “живут” в этом ДОМЕ и хотят смотреть через ДВЕРНОЙ ПРОЕМ во ВНЕШНИЙ МИР. Все они сгрудились около одного и того же ДВЕРНОГО ПРОЕМА, который на всех — один.  С одной стороны это очень хорошо, потому что Views “видят” через общий для всех ДВЕРНОЙ ПРОЕМ один и тот же ВНЕШНИЙ МИР, и, следовательно, наш UI является согласованным.
Все Views “видят” один и тот же  ВНЕШНИЙ МИР.
Но у нас есть одна проблема с нашей ВХОДНОЙ ДВЕРЬЮ — она широко открыта. И наш ДВЕРНОЙ ПРОЕМ полностью открыт.
У нас есть переменная var model, которая доступна любому Views и он может, например, найти и посмотреть на любую Card, а также может установить любое её свойство, например, isMatched, а это может испортить нашу игру:

Почему это может испортить нашу игру?
Возможно, что наша игра ведёт счёт и, если карты совпали (are matched), то игроку даются очки или ещё что-то, но если вы просто так можете зайти в игру и изменить свойство isMatched любой карты, то ваша карта метится как “совпавшая”, а очков вы никаких за это не получаете и т.д..
Так что какой-то плохой View-мошенник может разрушить целую игру для всех оставшихся Views.
И это происходит потому, что все смотрят на одну и ту же вещь.
С помощью переменной var model вы смотрите через открытый ДВЕРНОЙ ПРОЕМ на Model и тот факт, что наша ViewModel является классом class и является общей для всех Views делает её опасной.
И всё таки мы можем кое-что сделать, чтобы смягчить эффект беспокойства по поводу того, что этот класс class разделяется большим количеством Views, но сохранить при этом преимущества “разделения”.
И один из этих способов — вообще наглухо ЗАКРЫТЬ ВХОДНУЮ ДВЕРЬ.
Переменную var model мы можем пометить ключевым словом private:

Это означает, что переменная var model может быть доступна только внутри EmojiMemoryGame.
Она является private для этого класса class.
Это решает проблему плохого View-мошенник, который  может проникнуть в структуру struct Card и самостоятельно установить её свойство isMatched в Card, но похоже это чрезмерно хорошо решает проблему View-мошенника, потому что теперь ни один View НЕ сможет заглянуть за ДВЕРЬ. Никакой View теперь НЕ сможет НИКОГДА посмотреть на модель Model.
ДВЕРЬ закрыта и ВНЕШНИЙ МИР НЕ доступен ни для каких Views.
Определенно в этом и проблема.
Как нам найти золотую середину?
Ну. Один способ, каким мы можем это сделать, — это использовать немного другой private, который называется private (set):

Когда мы пишем private (set), то это соответствует тому, что ДВЕРЬ ЗАКРЫТА, но это СТЕКЛЯННАЯ ДВЕРЬ.
Синтаксическая конструкция private (set) означает, что только EmojiMemoryGame может модифицировать переменную var model, но любой может смотреть на model.
Это СТЕКЛЯННАЯ ДВЕРЬ.
СТЕКЛЯННАЯ ДВЕРЬ прекрасно обеспечивает защиту от View-мошенника, который может проникнуть в model и изменить свойство isMatched карты Card для увеличения счёта и всё такое.
Эта проблема решается private (set), но теперь никто не может выбрать (choose) какую-нибудь карту Card, потому что Views НЕ могут добраться через СТЕКЛЯННУЮ ДВЕРЬ до model и выбрать  (choose) карту Card, а это, возможно,  самое главное действия, которое Views хотят делать, когда используют “жест” Tap по карте Card.
И здесь на помощь нам приходят “Намерения” Intents.
Помните?  Мы говорили о ViewModels, и одной из обязанностей ViewModels является интерпретация “Намерений” Intent(s) пользователя, и именно это и происходит:

Я разместил небольшой комментарий и собираюсь написать здесь функции, которые позволят этим Views получить доступ к ВНЕШНЕМУ МИРУ.
——- 55-ая минута лекции ———
Следуя нашей аналогии, вы можете представить, что наша ДВЕРЬ является HIGH-TECH ДВЕРЬЮ, оборудованной Видео домофоном, и эти Views собираются нажать кнопку домофона и поговорить с ВНЕШНИМ МИРОМ, попросив выбрать (choose) эту карту Card.
Этой HIGH-TECH ДВЕРЬЮ является ViewModel, и она, очевидно, может напрямую говорить с model и сообщить ей о том, что нужно сделать.
Эти “Намерения” Intent(s) пользователя — это своего рода то, что Views “говорят” по домофону. То есть Views озвучивают то, что они хотят, чтобы произошло с нашей игрой.
Очевидно, что нам следует иметь функцию с именем choose ( card: Card) наподобие той, которая у нас есть в model :

Это “Намерение” Intent заключается в том, что пользователь должен выбрать эту карту card: Card прямо сейчас. Однако нам следует убедиться, правильно ли мы дали полное имя ТИПу карты card. Полное имя ТИПа состоит из нескольких частей MemoryGame<String>.Card:

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

Но имейте в виду, что нашей Model может быть, например, SQL база данных или что-то ещё, и мы должны будем использовать здесь кучу команд SQL базы данных, чтобы осуществить “Намерения” Intent(s) пользователя.
В нашем случае это очень простое приложение, просто демонстрационный пример, и, к счастью, мы можем легко выразить “Намерения” Intent(s) пользователя по выбору карты. Это будет работать.
Замечательно, что у нас есть private (set), благодаря которому мы можем видеть карты через model.cards, и выражать свои “Намерения” через  функцию choose с целью ИЗМЕНИТЬ ВНЕШНИЙ МИР. У нас есть СТЕКЛЯННАЯ ДВЕРЬ и мы можем смотреть сквозь неё на ВНЕШНИЙ МИР, и она защищает нас от ВНЕШНЕГО МИРА.
Но, возможно, мы хотим быть более закрытыми.
Например, мы хотим, чтобы ДВЕРЬ была ЗАКРЫТОЙ, то есть строгий private :

И вместо того, чтобы смотреть через СТЕКЛЯННУЮ ДВЕРЬ, мы будем использовать домофон с видеокамерой. Вы знаете, как работает домофон с видеокамерой: люди подходят к двери и вы можете их видеть через маленький видеоэкран.
По аналогии с маленьким видеоэкраном мы предоставляем переменные vars и функции funcs, которые позволят людям взглянуть на  нашу модель model ещё более ограниченными способами. Конечно, мы хотим, чтобы люди видели карты cards нашей модели model, так что мы создадим нашу собственную переменную var cards, которая также будет массивом MemoryGame.Card и которая вернет нам model.cards :

То же самое мы сделали и с функцией choose.
У нас очень простая Model, и поэтому нам так просто это делать, но ViewModel могла бы делать некоторую интерпретацию в этих переменных vars и функциях funcs, например, преобразование данных Model так, чтобы результат преобразования в большей степени подошел бы View, или, например, выполнила бы некоторую работу по обработке запросов из интернета, если данные Model приходят оттуда.
Если у вас есть выбор между увеличением сложности в ViewModel за счет преобразования данных и желанием сделать View как можно проще, то мы всегда будем искать компромисс в этом направлении. Мы хотим, чтобы наши Views были по возможности более простыми, так что на самом деле это “работа” ViewModel — представлять Model нашим Views наиболее подходящим для Views способом.
Конечно, мы должны убрать return при возврате значения для cards, так как это единственная строчка кода:

Прекрасно. Теперь у нас ПОЛНОСТЬЮ ЗАКРЫТАЯ ДВЕРЬ. И мы разрешаем доступ по видеодомофону. Поэтому я хочу пометить этот код с помощью

// MARK: — Access to the Model:

Здесь расположены функции и переменные vars, дающие доступ к переменным vars модели model, а ниже находятся “Намерения” Intent(s).
Между прочим, причина, по которой я расставляю здесь различные // MARK: — , заключается в том, что они будут показаны при отображении структуры вашего кода:

Вы видите все эти // MARK: — , которые обеспечивают что-то типа заголовков в списке функций func и переменных vars.
Лично я предпочитаю более ЗАКРЫТУЮ НЕ СТЕКЛЯННУЮ ДВЕРЬ, то есть private, но в зависимости от обстоятельств иногда имеет смысл использовать private (set) и  позволить людям смотреть через  СТЕКЛЯННУЮ ДВЕРЬ на модель model.
Но в любом случае у вас будет то, что называется “Намерениями” Intent(s). Это своего рода документация. Она позволяет Views знать, а точнее тем людям, которые пишут код для Views, что здесь находится то, что позволит им изменять модель model.
А что у нас за ошибка в коде?

Она говорит о том, что  у класса EmojiMemoryGame нет инициализаторов.
Что это означает?
Мы уже изучали инициализаторы, что же всё-таки происходит?
Если в классе class появляется такая ошибка, то, по сути, это означает, что у класса class есть переменная var, которая не инициализирована.
——- 60-ая минута лекции ———
И действительно, наша переменная var model не имеет начального значения:

Нам необходимо ее установить, то есть написать var model = “чему-то”, чтобы удовлетворить требованиям Swift, когда в классе class все переменные vars должны быть инициализированы.
Как мы будем инициализировать var model?
Мы должны указать ТИП MemoryGame<String>.Card переменной var model = и открыть круглую скобку ( :

В списке появляется инициализатор, для которого мы должны задать аргумент cards, и мы кликаем дважды на этом инициализаторе:

Действительно, этот инициатор игры MemoryGame<String> хочет иметь карты cards в качестве аргумента инициализатора. Почему ему нужны карты cards?
Если мы взглянем на структуру struct MemoryGame, то увидим, что у этой структуры также есть неинициализированная переменная var cards:

Если ты хочешь инициализировать игру MemoryGame, то нужно задать значение для переменной var cards.
Это в точности то же самое, что было у нас при инициализации CardView(isFaceUptrue):

… когда мы вынуждены были задать значение переменной isFaceUp при инициализации, так как она не была инициализирована непосредственно в структуре struct CardView.
Каждый раз, когда у вас есть неинициализированные переменные vars, ответственность за их инициализацию возлагается на того, кто создает эту вещь.
Но в нашем случае это фикция (фальшивка), потому что EmojiMemoryGame хочет остаться в стороне от создания карт cards, управление ими — лежат ли они “лицевой” стороной вниз (установка isFaceUp), совпадают ли они (установка isFaceUp). Всё это дело MemoryGame: решать, какие карты лежат “лицевой” стороной вниз, а какие — “лицевой” стороной вверх.
Реально, это сама MemoryGame хочет инициализировать карты cards.
Именно в ней хочется написать var cards : Array<Card> = …

Но проблема заключается в том, что MemoryGame в действительности не знает, например, сколько карт Card в игре. Так где же MemoryGame взять число карт?
Количество карт будет сообщено из ViewModel при попытке создания переменной var model через MemoryGame. Это самое удобное место для задания количества карт, когда при инициализации MemoryGame мы в качестве аргумента дадим НЕ карты cards, а число пар карт numberOfPairsOfCards:. Их может быть 5 или 6 или 2 пары каких-то карт Card:

Тогда в MemoryGame я мог бы сказать: “Хорошо!  Я создам это количество пар карт Card и установлю им свойства:”

То есть мы хотим создавать игру MemoryGame с совершенно случайным другим аргументом, который вовсе не является его переменной var cards, это просто некоторый кусочек информации.
Это очень распространенная практика, когда мы хотим создавать некоторые var  именно таким образом и делаем это с помощь специальной функции init.
Так что мы добавим в MemoryGame совершенно новую функцию init, но ей не будет предшествовать ключевое слово func, так что НЕ func init, а просто init: потому что инициализаторы inits по определению являются функциями, и мы дадим ей какой мы хотим аргумент. Мы хотим, чтобы этот аргумент имел имя numberOfPairsOfCards и ТИП Int:

Инициализатор init ничего НЕ ВОЗВРАЩАЕТ, потому что он только инициализирует все ваши переменные vars. Именно это делает init.
Но что действительно круто — это то, что у вас может быть множество инициализаторов inits, причем каждый с различными аргументами.
Если у вас есть какие-то другими способы создания игры MemoryGame, вы можете воплотить их в других inits.
И мы видели это и раньше.
Давайте вернемся к ContentView, там есть у нас RoundedRectangle, давайте посмотрим на его инициализаторы:

Когда при создании прямоугольника с закругленными углами RoundedRectangle мы открыли круглую скобку,  нам показали сразу 4 различных способа создания  RoundedRectangle : с помощью радиуса закругления углов cornerRadius, с помощью размера углов cornerSize или некоторого стиля style или сочетанием этих аргументов. Всего здесь 4 различных инициализатора inits, для того, чтобы создать прямоугольник с закругленными углами RoundedRectangle.
В нашем коде MemoryGame тоже самое, но у нас только один init.
Что наш init должен делать?

Нам необходимо инициировать все наши переменные vars, потому что не разрешено иметь в MemoryGame неинициализированные переменные.
Прежде чем погрузиться в это, давайте начнем с создания карт cards как пустого массива:

Другими словами, я вызываю для массива Array<Card> инициализатор init без аргументов, который создает пустой массив Array<Card>.
Теперь наш массив карт cards пуст, что вполне удовлетворяет требованиям инициализации.
Но, конечно, нам нужно больше, нам необходимо создать numberOfPairsOfCards пар карт и разместить в этом массиве cards: Array<Card>.

——- 65-ая минута лекции ———
Для этого нам понадобится for цикл и сейчас вы впервые увидите for цикл в Swift. Сначала идет ключевое слово for, затем переменная итерации, которую мы назовем pairIndex, то есть индекс пары, поскольку я собираюсь что-то делать в том for цикле для каждой пары.
Затем следует ключевое слово in:

Цикл for in — это единственный for цикл в Swift.
Затем следует эта iteratablething, которую мы видели уже раньше.
Это нечто, что мы можем подвергнуть итерации, и в большинстве случаев это массив Array.
В нашем случае мы собираемся использовать ту же самую iteratablething, которую мы использовали в ForEach в ContentView:

Это диапазон Range, который также, как и массив Array, является iteratablething, но в моем случае я хочу использовать диапазон Range от 0 до числа пар карт numberOfPairsOfCards, не включая само число пар карт numberOfPairsOfCards:

Вот как выглядит for in цикл в Swift. Он проходит через целые числа 0, 1, 2, … и до numberOfPairsOfCards, но само число numberOfPairsOfCards не входит.
Если у нас 2 пары карт, то for in цикл проходит через 0 и 1 и останавливается.
Внутри этого цикла я должен добавить две карты Card с помощью функции append, это функция в массиве Array:

Я добавил пару карт Card, но, конечно, я не могу создать карту Card просто с помощью  открывающей и закрывающей круглых скобок Card( ), так как это неправильно, ибо в структуре struct Card есть неинициализированные переменные.
Но если я просто использую открывающую скобку (, то получу прекрасный инициализатор, в котором инициализируются все переменные:

Я кликаю дважды и получаю инициализатор, в котором аргументами являются переменные vars структуры struct Card:

Мы можем это сделать для обеих карт:

Мы получили эти прекрасные инициализаторы, потому что Card — это структура struct, а у структуры есть “бесплатный” инициализатор, который инициализирует ВСЕ переменные vars этой структуры.
Между прочим, для класса class мы также получаем “бесплатный” инициализатор, но он НЕ инициализирует НИКАКИЕ переменные vars этого класса.
Мы инициализируем классы с помощью = “имя класса” ( ) ИЛИ создаем свой собственный инициализатор.
Но для структуры чаще всего нет необходимости в создании собственного инициализатора, так как мы “бесплатно” получаем прекрасный инициализатор с аргументами, соответствующими ВСЕМ переменным vars этой структуры.
Я начинаю создание карты Card, конечно, с того, что карта лежит “лицевой“ стороной вниз, так как это начало игры, и она ещё НЕ СОВПАЛА ни с какой картой:

Но чему равен content?
Это становится очень интересным.
Мы знаем, что обе карты должны быть именно такими, но что такое CardContent?
Определенно это проблема.
Я создам локальную переменную var content = …, которая будет чему-то равна и использую её при инициализации пары карт:

Одно и то же содержимое content на обеих картах, ведь это ПАРА карт Card.
Но похоже, что в MemoryGame мне не удастся создать содержимое content, так как content имеет ТИП CardContent, а для MemoryGame это “Не важно, какой” ТИП.
MemoryGame даже не знает, чем реально это может быть:  изображением Image, или целым числом Int, или строкой String, MemoryGame реально не может этого знать. Как она может создать то, чего не знает?
В MemoryGame нет никакого способа сделать это.
А кто знает, как создать содержимое content для карты Card?
Это знает EmojiMemoryGame:

Именно EmojiMemoryGame создает MemoryGame с содержимом карт CardContent в виде строки String, по-видимому он и должен знать, как создать содержимое content для каждой пары карт. По крайней мере мы должны дать ему возможность создать содержимое карт content.
И мы сделаем это с помощью функции.
Я просто добавлю ещё один аргумент в мой инициализатор init для MemoryGame и назову его cardContentFactory, а ТИПом этого аргумента будет функция, которая берет целое число Int и возвращает ТИП CardContent:

Напоминаю, что возвращаемый ТИП CardContent — это “Не важно, какой” ТИП.
На вход функции cardContentFactory поступает целое число — номер пары карт pairIndex, который дает понять, для какой пары карт создается содержимое content.
Тот кто, создает игру MemoryGame с содержимом карт CardContent должен дать функцию, возвращающую содержимое определенной пары карт, которое может оказаться в случае, если CardContent соответствует Image, то изображением Image, в случае, если CardContent соответствует String, то строкой String, но меня (MemoryGame) это не волнует.
Так что я могу вызвать функцию cardContentFactory с аргументом pairIndex для получения содержимого карт content:

ТИП аргумента функции cardContentFactory задан как ТИП ФУНКЦИИ, но это обычный ТИП, такой же как ТИП String, потому что функции в Swift — это “граждане” первого класса, как и другие ТИПЫ (String, Int, Double) и в случае ТИПА ФУНКЦИИ нет ничего особенного — она как все остальные ТИПы. Вы можете передавать ФУНКЦИИ повсюду.
——- 70-ая минута лекции ———
Опять же, как вы можете себе представить, в языке ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ возможность передавать функции — это фундаментальное свойство языка. Это своего рода основная часть функционального программирования:

Вы не должны бояться этого. В других языках программирования передача функции повсюду может превратиться в “пытку”, когда передаются указатели на них и все эти сумасшедшие вещи.
В Swift же все просто: вы буквально объясняете ТИПы аргументов и ТИП возвращаемого значения и — БУМ! — функция передается повсюду.
Теперь у нас появилось предупреждение, выделенное ЖЕЛТЫМ цветом. Мы уже видели ошибки, выделенные КРАСНЫМ цветом, это соответствует тому, что код не компилируется и это ужасно.
Если предупреждение имеет ЖЕЛТЫЙ цвет, то код компилируется, но вы непременно захотите избавиться от этих предупреждений, потому что очень часто они приводят к будущим проблемам, но не к сиюминутным.

Что нам сообщает это предупреждение?
Переменная var content нигде не изменяется, подумайте об изменении на константу let.
По сути нам говорят, что не стоит называть “переменной” content, так как она НЕ ИЗМЕНЯЕТСЯ, ей не следует быть var и вместо этого ключевого слова Swift предлагает ключевое слово let. Эту замену var на let можно сделать непосредственно в коде, а можно кликнуть на кнопке «Fix”. В этом случае произойдет автоматическая замена var на let:

let — прекрасное ключевое слово, потому что позволяет очень хорошо читать код по-английски.
Каждый раз, когда у вас есть переменная var, которая фактически не изменяется, то есть по сути является константой, вам ВСЕГДА следует использовать ключевое слово let.
Я хочу обратить ваше внимание ещё на одну вещь. Мы не указали какой ТИП имеет константа let content, мы НЕ указали ТИП. Может следовало бы это сделать и указать ТИП CardContent, так как функция cardContentFactory возвращает ТИП CardContent?

Но НЕТ НЕОБХОДИМОСТИ делать это, есть специальная часть Swift, которая просто это делает вместо вас, она “выводит из контекста” (inferring) этот ТИП, если может, и мы увидим это с другой стороны через мгновение, когда будет вызывать этот инициализатор init:

Давайте вернемся на другую сторону, в EmojiMemoryGame, туда, где мы создаем игру MemoryGame. Мы разместим код создания MemoryGame на отдельной строке, чтобы было больше места и добавим к первому аргументу с именем numberOfPairsOfCards второй аргумент с именем cardContentFactory:

Значением  второго аргумента должна быть функция, которая берет Int, им является индекс пары карт pairIndex, и возвращает CardContent, но в нашем случае мы знаем, что функция должна вернуть String, так как создается карточная игра MemoryGame<String> с картами, содержимом которых является строка String.
Давайте создадим такую функцию и я дам ей имя createCardContent. Она берет в качестве аргумента Int, которым является pairIndex, и мы знаем, что она возвращает CardContent, который в нашем случае является строкой String:

Эта функция будет возвращать какой-нибудь эмоджи:

У нас будет один и тот же эмоджи на всех картах. Эта функция возвращает строку “😀”.
Мы можем использовать функцию createCardContent в качестве значения второго аргумента при инициализации игры MemoryGame<String> :

Функция createCardContent подходит нам по всем критериям:  на берет Int и возвращает String.
Все абсолютно законно и вы не видите ни ошибок, ни предупреждений.
Замечательно.
Тем не менее мы никогда не будем так делать, потому что мы не хотим создавать каждый раз дополнительные маленькие функции.
Вместо этого мы будем ВСТРАИВАТЬ (inline) эти функции прямо в этот код.
Следите внимательно за мной, я пошагово покажу вам, как взять обычную функцию (в нашем случае функцию createCardContent) и встроить ее в качестве значения в код.
Встроенная (inlining) функция в Swift называется ЗАМЫКАНИЕМ (closure), потому что она действительно “захватывает” информацию из своего окружения, которая требуется ей для работы. Об этом мы поговорим позже, но сейчас вы можете думать о ЗАМЫКАНИЯХ как о встроенных  (inlining) функциях.
Мы выделяем эту функцию целиком, за исключением имени, потому что для встроенной функции не нужно имя:

Затем я выполняю Cut (вырезать) и Paste (вставить) в инициализатор в качестве значения для аргумента cardContentFactory вместо имени функции:

Это почти работает, но есть одна вещь, которую я должен всегда делать, выполняя операцию встраивания функции. Я должен выделить открывающую фигурную скобку {:

Cut (вырезать) её, поставить на её место ключевое слово in и Paste (вставить) открывающую фигурную скобку { в самое начало встроенной функции:

По сути теперь фигурные скобки {  } обрамляют полностью нашу встроенную функцию:

——- 75-ая минута лекции ———
 Именно поэтому мы вынесли открывающую фигурную скобку {  в начало, перед аргументами и ТИПом возвращаемого значения, мы используем фигурные скобки, чтобы выделить встроенную функцию.  Нам больше не нужна func createCardContent:

И мы получаем прекрасный законный способ для встраивания функции:

Видите? Нет никаких ошибок или предупреждений.
Между прочим, вы, возможно, уже распознали это ключевое слово in, мы уже использовали его в нашем ContentView c ForEach:

Здесь ключевое слово in используется вместе с этим аргументом index.
Это обретёт больший смысл, как только мы закончим с этим:

Что я имею в виду под тем “как только мы закончим с этим”? Разве мы ещё не закончили?
Не совсем.
Подобно тому, что в MemoryGame мы не указываем ТИП константы content, которая имеет ТИП CardContent, мы можем попытаться упростить выражение для нашей встроенной функции. Нам не пришлось  писать let content: CardContent, потому что в Swift действует механизм “вывода ТИПа из контекста” (inference):

Присутствие в Swift механизма “вывода ТИПа из контекста” (inference) замечательным образом сказывается на языке программирования со строгой типизацией, когда всё ДОЛЖНО ИМЕТЬ ТИП: каждая переменная var должна иметь ТИП, каждая константа let, абсолютно всё должно иметь ТИП. Это очень обременительно, но механизм “вывода ТИПа из контекста” (inference) существенно облегчает нам “жизнь” и делает бремя “обязательной типизации” не таким тяжелым.
Может быть механизм “вывода ТИПа из контекста” (inference) может нам в чем-то помочь при задании встроенной функции?
Да, он нам очень может помочь, потому что мы знаем ТИП аргумента cardContentFactory из инициализатора:

То есть встроенная функция берет Int и возвращает CardContent, значит нет необходимости в указании ТИПа pairIndex и нет необходимости в указании ТИПа возвращаемого значения, потому что Swift “выведет эти ТИПы из контекста” (infer):

Видите? Никаких ошибок, никаких предупреждений, всё законно.
На самом деле вам даже не нужны круглые скобки вокруг pairIndex :

Видите? Уже знакомая нам синтаксическая конструкция: pairIndex in и мы уже видели index in в ForEach:

Потому что  и то, и другое являются встроенными функциями, так как список Views, которым является ForEach, имеет тот же самый синтаксис.
Но мы ещё не закончили, так как в нашей встроенной функции код состоит из одной строки, так что, конечно, мы можем избавиться от ключевого слова return:

Итак, у нас фигурные скобки { } и последний аргумент в инициализаторе, следовательно, мы можем сделать с нашим последним элементом то же самое, что мы сделали с последним аргументом ForEach, с последним аргументом HStack и с последним аргументом ZStack.
Мы избавимся от метки cardContentFactory последнего аргумента и разместим фигурные скобки снаружи вызова инициализатора:

Мы завершили наши преобразования такой маленькой “обтекаемой” функцией.
Но это ещё не всё, потому что мы возвращаем просто улыбающийся смайлик 😀, который никак не зависит от pairIndex, который нам не нужен, но мы не можем просто удалить pairIndex, мы должны пометить его отсутствие с помощью _ (подчеркивания):

Тем самым я говорю: “Да, я знаю, что эта функция имеет аргумент, но он мне не нужен”.

В таком случае используется _ (подчеркивание). Мы знаем, что если мы видим символ  _ (подчеркивания) в Swift, то это означает, что то, что заменено символом _ (подчеркивания) для нас не имеет никакого значения. Это своего рода символ “неиспользования”.
В нашем конкретном случае мы не используем аргумент pairIndex.
Как видите очень простой синтаксис:

Вам следует привыкнуть к этому, потому что мы будем именно так будут передаваться функции как аргументы. Мы уже видели это в View повсюду, где присутствуют фигурные скобки { } , и это есть функциональное программирование, когда мы передаем множество функций как аргументы других функций.
А что если я хочу возвращать разные эмоджи для каждой пары карт?
Я вовсе не хочу, чтобы все пары карт имели один и тот же улыбающийся смайлик 😀, это слишком упрощает нашу игру и делает неинтересной.
Как мне сделать игру более сложной?
Прежде всего вместо нашего кода при создании игры я буду просто вызывать функцию createMemoryGame, в которую перенесу наш код:

Функция createMemoryGame возвращает нам игру MemoryGame<String>.
Мы хотим сделать игру более сложной, поэтому усложним нашу встроенную функцию.
Восстанавливаем pairIndex in, но для получения различных эмоджи мы должны создать небольшой массив эмоджи emojis, который должен быть массивом строк Array <String>:

Мы хотим сделать наш массив emojis равным некоторой константе, которая является массивом строк и я покажу вам, какой используется синтаксис для задания массива-константы:

Открывается квадратная скобка, затем через запятую перечисляются все элементы, которые вы хотите разместить в массиве-константе, и квадратная скобка закрывается.
——- 80-ая минута лекции ———

У меня будет массив эмоджи, так что я вернусь к теме Хэллоуина и получу эмоджи “Приведение” и эмоджи “Тыква”:

Итак, у нас есть массив строк String, потому что эмоджи — это строки.
И при возврате MemoryGame в этой маленькой фабрике карт мы вернем элемент массива emojis с индексом pairIndex:

Здесь видно, как осуществляется доступ к элементу массива: в квадратных скобках указывается индекс элемента массива, в нашем случае это pairIndex, который принимает значения 0 и 1. Так что первой парой карт будет пара карт с “Привидением”, а вторая пара карт — с “Тыквой”.
Вы видите, что в коде функции createMemoryGame нет никаких ошибок, но есть ошибка в самом верху.  О чем она говорит?

Вы не можете использовать функцию экземпляра класса createMemoryGame при инициализации свойства. Инициализаторы свойств (Property initializers) запускаются перед тем, как доступен self.
Что это значит?
Я говорил вам, в Swift мы должны инициализировать все переменные, то есть что-то им присвоить.
Но в Swift действует и ещё более строгое ограничение: мы не можем использовать никакие функции экземпляров класса class или структуры struct до тех пор, пока не будут инициализированы все переменные vars.
Это похоже на “Уловку-22”. Я хочу использовать функцию createMemoryGame экземпляра класса class EmojiMemoryGame, но не могу этого сделать, потому что экземпляр класса class EmojiMemoryGame, то есть self, еще не инициализирован.
Замкнутый круг какой-то!
Как нам выйти из этого положения?
Мы превратим нашу функцию экземпляра класса createMemoryGame в static функцию:

Так, static функция — это функция ТИПА EmojiMemoryGame, а НЕ функция ЭКЗЕМПЛЯРА класса.
Если раньше функция createMemoryGame посылала сообщения ЭКЗЕМПЛЯРУ класса class EmojiMemoryGame, то теперь она посылает сообщения ТИПу EmojiMemoryGame.
Надеюсь, все знают, что в объектно-ориентированном смысле означает “экземпляр” класса class.
Для функций ТИПа используется специальный синтаксис : задаете имя ТИПа, затем “точка” и имя функции ТИПа:

Такой синтаксис работает только для static функций.
Это функция ТИПа, а не функция ЭКЗЕМПЛЯРА класса class EmojiMemoryGame, она работает только с непосредственно с самим ТИПом.
Мы уже использовали это в ContentView: Color.orange, Font.largeTitle. Здесь Font и Color — это ТИПы:

В этом случае orange и largeTitle — это не функции ТИПА а переменные vars ТИПА, но вы можете сделать static и функции, и переменные vars.
Давайте посмотрим это в документации и посмотрим, что происходит.
Как нам попасть в документацию из кода?
И это очень крутая ”фишка”.
Удерживаете клавишу Option. Если вы мышкой перемещаетесь по коду, то вас сопровождает повсюду вопросительный ? знак:



Я кликаю (Option + Click) на Font и мне дают краткое описание того, что такое Font:

Но внизу мне предоставляют ссылку на документацию.
Я кликаю на этой ссылке и — БУМ! — меня выводят на документацию по Font:

Это о том, как добраться до документации.
Но, конечно, мы можем добраться до документации и через меню: Window -> Developer Documentation:

Но обычно мы используем Option + Сlick для того, чтобы добраться до документации.
Давайте взглянем на документацию по Font :

Вы видите здесь static let largeTitle, это static константа ТИПа Font.
Вы здесь видите и другие static константы ТИПа Font, возможно, они вам пригодятся в вашей Домашней работе. Вы можете поэкспериментировать с тем, как они изменяют текст.
Это встроенный шрифты и очень желательно их использовать, потому что использование одних и тех же шрифтов при переходе от одного приложения к другому, облегчает восприятие UI пользователем. Эти шрифты имеют различные стили, но в любом случае они будет одними и теми же в разных приложениях.
Мы можем искать в документации всё, что нам нужно. Для этого достаточно набрать поисковую строку в верхней части экрана, например, Array.
Нам предлагают список всего, что подходит к слову “Array”:

Нам предлагают список всего, что подходит к слову “Array”.
Первый элемент предложенного списка скорее всего будет классом class или структурой struct с именем, совпадающим с поисковой строкой. В нашем случае это структура Array и мы кликаем на ней:

Вам определенно нужно прочитать всё, что касается массива Array, чтобы познакомиться с тем, что вообще может делать массив. Это очень поможет вам при выполнении вашего Домашнего Задания.
——- 85-ая минута лекции ———
Если вы прокрутите всё это и посмотреть. Конечно, я не ожидаю, что вы разберетесь во всем, как это работает, но определенно у вас есть возможность найти какую-то определенную функцию, которая может помочь вам в вашей Домашней работе.
То же самое с View.

Давайте взглянем на View:

Здесь представлено описание View и мы уже кое-что знаем об этом, но безусловно нам предстоит ещё многое узнать о View:

 У View очень много функций и переменных vars, для удобства они разделены на секции.
Пара из них очень интересные. Это Layout, мы будем изучать это свойство подробно на следующей недели:

Мы можем найти у Layout функцию padding:

 View есть ещё переменная Rendering:

Rendering связан с масштабирование scale, вращением rotate, с “размытием” blur и другими операциями над View.
Всё это поможет вам найти необходимое для вашего Домашнего Задания. Часть Домашнего Задания и состоит в том, чтобы вы уверенно маневрировали по документации.
Некоторые вещи вам будут абсолютно непонятны, например, @State. Наверняка вы подумаете, что это за зверь такой?
На самом деле я вовсе не ожидаю, что вы будете изучать что-то, просто читая  документацию. Я ожидаю, что вы должны знать, что документация существует и вы можете найти там что-то полезное.
Итак, возвращаемся к коду, к нашей замечательной static функции:

И опять, никаких ошибок, никаких предупреждений.
Мы создали своего рода вспомогательную (utility) функцию ТИПА для создания игры MemoryGame.
Теперь у нас есть ViewModel, это класс class EmojiMemoryGame, он смотрит на нашу Model через переменную var model.
Что касается нашей Model, то мы ещё не научили её «играть» при выборе карты, но по крайней мере Model может сказать, какая карта была выбрана пользователем:

Давайте вернемся к нашему View и используем здесь нашу ViewModel.
Помните? Что наше View ВСЕГДА хочет использовать ViewModel для доступа к тому, что есть в Model. Не забывайте, что первостепенной задачей View в этом Мире является ОТОБРАЖЕНИЕ ТЕКУЩЕГО СОСТОЯНИЯ Model.
Что бы не находилось в Model, View ВСЕГДА хочет это показывать.
Давайте начнем с CardView. В настоящий момент у CardView одна единственная переменная var isFaceUp :

Но на самом деле CardView должен получать isFaceUp из карты, которую показывает CardView и я заменю переменную var isFaceUp: Bool на var card: MemoryGame<String>.Card:

ТИП MemoryGame<String>.Card получился слишком длинным и, естественно в Swift есть способ тот же ТИП представить более лаконично, Мы поговорим о этом на следующей неделе.
Естественно я должен заменить isFaceUp на card.isFaceUp и вместо того, чтобы всегда показывать “Привидение”, я заменю его на card.content:

Относительно card.content, это действительно очень круто, потому что content в нашей игре MemoryGame имеет ТИП CardContent, который является “Не важно, какой” ТИПом, то есть мы даже не знаем его содержимое:

Но, конечно, в EmojiMemoryGame мы точно знаем, что это будет строка String:

Именно поэтому в нашем ContentView мы имеем MemoryGame<String>.Card и, следовательно, card.content имеет ТИП String, а Text как раз и отображает строки String:

В верхней части этого кода у нас ошибка, и понятно, что у CardView больше нет переменной isFaceUp, нам нужно дать ему какую-нибудь карту card из нашей ViewModel:

Как нам найти способ обеспечить CardView какой-нибудь картой card?
Мы возьмем карты из нашей ViewModel.
Для этих целей нам нужна переменная var, которую я назову viewModel:

И опять, вы не должны называть такую переменную var viewModel, также как вы не должны называть переменную var model:

Но я умышленно выбрал эти два названия в учебных целях, чтобы вы видели ГДЕ я получаю доступ к model:

 И то же самое в нашем случае. Называя переменную var viewModel, я покажу, ГДЕ я получаю доступ к ViewModel.
Какой ТИП у переменной var viewModel?
EmojiMemoryGame — это  и есть ТИП нашей viewModel, класс class EmojiMemoryGame:

Если EmojiMemoryGame — это класс class, то переменная var viewModel представляет собой указатель (pointer) на этот класс. Если у меня есть другие Views, которые имеют доступ к viewModel, то у них также есть указатели (pointers) на него, так что должен быть где-то  один единственный экземпляр класса class EmojiMemoryGame, на который все будут ссылаться.
Где мы будем создавать экземпляр EmojiMemoryGame?
Мы будем создавать его там, где создается ContentView.
Мы будем делать то же самое, что мы делали при создании CardView с переменной var isFaceUp, то есть то есть использовали выражение CardView (isFaceUp: true).
С ContentView и переменной var viewModel мы, по сути, собираемся сделать то же самое.
Где создается ContentView?
И пришло время немного погрузиться в код, который пришел вместе с шаблоном.
Помните? Я говорил вам, что в этих AppDelegate и SceneDelegate находится шаблонный код?
Если мы кликнем на SceneDelegate, то увидим здесь много хлама, о котором поговорим позже, но здесь есть очень важная строка кода, которая создает contentView, а он в свою очередь используется главным Viewwindow:


——- 90-ая минута лекции ———
И вы видите, что этот contentView ”жалуется”, что пропущен аргумент и этим аргументом является viewModel:

SceneDelegate уже знает, что там находится переменная var viewModel и что она не инициализирована, так что мы должны установить ее при создании contentView. Нам необходимо назначить какое-то значение аргументу viewModel:

Я использую переменную let game и назначу ей значение EmojiMemoryGame ():

EmojiMemoryGame — это класс class, и у него есть “бесплатный” инициализатор init ( ), который ничего не инициализирует, но к счастью, EmojiMemoryGame уже инициализировал свою единственную переменную model:

Так что код в SceneDelegate будет работать:

В этом коде мы получили game для передачи в качестве значения аргумента viewModel.

Между прочим, если мы вернемся в ContentView, то увидим в самом низу кода аналогичную “жалобу” :

Это “клей”, который приклеивает код к серой области справа (Canvas) и который мы скрыли “с глаз долой” в самом начале этого демонстрационного примера.
Он создает ContentView и показывает его на Canvas.
Здесь также при создании ContentView необходим аргумент viewModel.
Я создам значение для этого аргумента “на лету” с помощью EmojiMemoryGame():

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

В данный момент мы просто показываем 4 карты: 0,1,2,3.
Мы используем диапазон Range, но я говорил вам, что на этом месте может быть любая iteratablething (то, что поддается итерации).
А как насчет того, чтобы использовать viewModel.cards?

Это массив Array<MemoryGame<String>.Cards>, так что это должно работать?
Но это НЕ работает.

Нам сообщают, что не могут преобразовать массив Array<MemoryGame<String>.Cards> в ожидаемый аргумент, имеющий ТИП диапазона целых чисел Range<Int>. То есть компилятор всё ещё думает, что хочу работать с диапазоном целых чисел Range<Int>.
Я сбил вас с толку, когда сказал, что может быть ЛЮБАЯ iteratablething (то, что поддается итерации). На самом деле требуется уточнение: это может быть ЛЮБАЯ iteratablething, но элементы, по которым идет итерация,  должны быть тем, что называется Identifiable (идентифицируемыми).
Если элементы, по которым идет итерация, не являются диапазона целых чисел Range<Int>, то они должны быть Identifiable (идентифицируемыми).
Итак, почему?
Почему они должны быть Identifiable?
Например, допустим, мы хотим анимировать наши карты viewModel.cards и передвигать их повсюду, передвигать в разном порядке или что-то наподобие этого.
ForEach (viewModel.cards) необходимо идентифицировать, чем является каждая отдельная  карта, то есть КТО есть КТО, потому что CardView, которое создается для каждой карты, должно быть четко синхронизировано с картами viewModel.cards.
Да, элементы массива viewModel.cards должны быть Identifiable, но в настоящий момент это НЕ ТАК.
Действительно, взглянем на viewModel.cards.
Это массив Array<MemoryGame<String>.Cards> в EmojiMemoryGame:

Если вы посмотрите на карты Card в MemoryGame, то увидите, что они НЕ Identifiable и нет никакого способа их идентифицировать:

Сейчас, фактически, две карты, которые совпадают, будут теми же самыми, потому что у них один и тот же content, они могут иметь то же самое свойство isFaceUp.
Нет никакого способа их идентифицировать их.
У Swift есть формальный механизм для идентификации чего-то, что делает это что-то Identifiable. Я называю этот механизм constraints and gains (ограничения и выгоды).
Это когда вы заставляете структуру struct делать определенные вещи, вы, по существу,  ограничиваете структуру struct, заставляя её делать определенные вещи, и она это делает, но взамен она приобретает определенные возможности.
О том, как работает механизм constraints and gains (ограничения и выгоды), мы поговорим на следующей неделе.
Мы уже использовали constraints and gains (ограничения и выгоды), когда объявили нашу структуру struct ContentView как структуру, которая “ведет себя как” View:

Это constraints (ограничения), так как вынуждены реализовывать переменную var body: some View. А выгода в том, что теперь наш ContentView получает всё, что может делать View.
Это constraints and gains (ограничения и выгоды) в структуре struct ContentView.
Мы сделаем то же самое со структурой struct Card.
Мы скажем, что constraint (ограничение), для этой структуры Card состоит в том, что она является Identifiable:

Identifiable, как и View являются тем, что называется протоколом protocol, и это “сердце” механизма constraints and gains (ограничения и выгоды).
Мы будем много говорить о протоколах protocol на следующей неделе.
К сожалению, в случае с Identifiable, я не получаю слишком много gains (выгод), за исключением того, что эта вещь получает возможность быть идентифицированной.
В случае с Identifiable constraint (ограничение) состоит в том, что вы должны иметь переменную var с именем id:

К счастью id может иметь любой ТИП. Я сделал мой id целым числом Int, но это может быть и строка String и всё что угодно, что может сделать эту вещь идентифицируемой.
Конечно, как только я добавил ещё одну переменную var id, мои карты Card (isFaceUp …) не инициализируют все переменные vars.
Для обеих карт мне нужно добавить id и для их инициализации я использовал pairIndex * 2 и pairIndex * 2 +1, потому что я хочу, чтобы каждая карта имела свой идентификатор:

Теперь все карты имеют уникальные идентификаторы. Они полностью Identifiable, а это то, что нам нужно.
——- 95-ая минута лекции ———
Мы просто должны убедиться, что все эти карты Card — уникально Identifiable, так как они будут повсюду перемещаться и анимировать.
Кстати, немного раздражает, что при создании карты Card каждый раз мы должны задавать все переменные vars. Я могу задать значения по умолчанию для некоторых vars непосредственно в самой структуре struct Card и убрать эти переменные при создании карты Card :

Вам разрешается инициализировать некоторые из ваших переменных vars таким способом и не использовать их при создании карт Card. Это делает код более понятным.
Теперь, когда всё у нас Identifiable, мы можем вернуться к нашему ContentView. Мы получили Identifiable карты viewModel.cards и ошибка исчезла:

Конечно, здесь не должен быть index диапазона Range,  этим аргументом должна быть карта card из массива viewModel.cards и вы, конечно, знаете, что это встроенная (inline) функция и её аргументом является card in и мы можем передать card в CardView:

ForEach проходит через все карты в массиве viewModel.cards, и для каждой карты card создается CardView.
И на этом всё.
Вот как мы подсоединяем нашу Model к View через ViewModel.
Наша ViewModel по сути обеспечивает “окно” или “портал” на нашу Model через этот массив cards и через choose (card: ), который мы пока еще не использовали, но собираемся это сделать:

Мы смотрим на Model:

И наш View просто ВСЕГДА будет отображать Model:

Сегодня у нас время заканчивается, и я не смогу показать вам АВТОМАТИЧЕСКОЕ отображение (auto-reflection) Model, выполняемое View, но мы рассмотрим это в первую очередь на Лекции на следующей неделе.
Однако мы проделали ключевую часть этой работы, и сейчас сможем по крайней мере увидеть, что View ВСЕГДА рисует то, что находится в Model.
View получает данные Model через ViewModel, но что бы он не получил, он ВСЕГДА рисует именно это.
Давайте посмотрим на всё это в действии.
Запустим приложение и получим 4 карты, лежащие “лицевой” стороной вниз:

Почему именно 4 карты и почему они лежат “лицевой” стороной вниз?
Потому что когда мы создавали MemoryGame в ViewModel, мы указали 2 пары карт, то есть всего 4 карты:

И в MemoryGame мы начинаем с того, что все карты лежат “лицевой” стороной вниз:

Давайте изменим в нашей Model false на true:

Теперь в нашей Model мы создаем карты, все лежащие “лицевой” стороной вверх.
Давайте посмотрим, как это повлияло на наше View:

Ух ты! Получилось. Мы видим два типа карт с теми эмоджи, которые мы задали для них.
Мы можем добавить еще один эмоджи, как-то связанный с Хэллоуином, это может быть “Паук” (spider):

И мы можем теперь отображать 3 пары карт:

Но мы можем сделать более крутую вещь, и вместо 3 задать emojis.count, так как emojis — это массив Array<String>. Но, между прочим,  я могу убрать ТИП у переменной var emojis, Swift будет “выводить ТИП из контекста” (infer):

Запускаем приложение:

Да, мы получили всё, что хотели.
Итак, наше View ВСЕГДА отображает ВСЕ, что видит в нашей Model.
Это здорово!
А как насчет возможности выражать “Намерения” Intent(s)?
Например, такого “Намерения” (Intent) как “я хочу выбрать карту”?
Я кликну (Tap) на карте и хочу, чтобы она была выбрана для дальнейшей игры.

Это также легко реализовать, потому что в View у нас есть эта var viewModel, и я могу к каждой карте добавить такую маленькую вещь, как onTapGesture:

 onTapGesture — это функция, которая в качестве аргумента берет другую функцию, у которой нет аргументов и она ничего не возвращает:

Что я хочу сделать в onTapGesture?
Я хочу попросить мою viewModel что-то сделать для меня, то есть это “Намерение” Intent, которое заключается в том, чтобы выбрать эту карту:

onTapGesture (perform🙂 есть у ВСЕХ Views, все они  знают, как работать с этим.
Так как здесь единственный аргумент и это последний аргумент, то нам просто не нужна метка аргумента “perform”. В большинстве случаев, чтобы сделать код более читаемым, мы размещаем встроенную функцию на отдельной строке:

——- 100-ая минута лекции ———

Но вы видите ошибку, и это очень интересная ошибка, потому что, возможно, через полтора месяца эта ошибка больше не будет появляться. Это будет исправлено или изменено в SwiftUI, а точнее в Swift.
Дело в том, что все изменения в Swift проходят процесс общественного рассмотрения, и эта ошибка тоже прошла через это и получила подтверждение тому, что она будет исправлена.
Но что по сути здесь происходит?
Иногда, когда у вас есть эти встроенные (inline) функции, то вам необходимо перед ними ставить self. :

Таким образом Swift узнает в точности, что происходит. Я не буду сегодня детально объяснять, что происходит из-за отсутствия времени, я объясню всё это на следующей неделе или неделей позже.
Но что эта ошибка говорила о том, что необходимо разместить self. прямо перед встроенной функцией.
Я рекомендую каждый раз, когда встречаете такую ошибку, требующую явного размещения self для того, чтобы осуществить явный “захват” (capture) семантического контекста, кликать на кнопке “Fix”:

Это размещает self. прямо перед встроенной функцией:

Вы можете поставить self. впереди любой переменной var в любое время. Это никому не навредит. Поэтому некоторые люди выбирают стратегию ставить повсюду self., чтобы не встречаться с этой ошибкой.
Но принимая во внимание, что через пару месяцев эта ошибка уйдет навсегда и не будет необходимости расставлять self., нет смысла придерживаться такой  стратегии.
Но пока я не знаю, какая стратегия правильная, но на этом курсе если вы видите ошибку, требующую явного размещения self для того, чтобы осуществить явный “захват” (capture) семантического контекста, кликать на кнопке “Fix” и self. будет размещена в нужном месте.
Давайте проверим, работает ли наше “Намерение”. Когда мы кликаем на эти карты, то срабатывает “жест” Tap и мы должны увидеть на консоли, какая карта выбрана:

Кликаем на карте и действительно что-то появляется в нижней части экрана.

Там находится “Область отладки” и “Консоль”, но есть специальная кнопка, которая позволяет скрыть “Область отладки” :

Мы кликаем на этой кнопке и я “Область отладки” скрывается, оставляя в нижней части экрана только “Консоль”.
Мы кликаем на различные карты и получаем различные результаты:

Появление карт на консоли обусловлено строкой кода в нашей Model:

Так что наш View способен добраться до кода в Model, просто высказывая своих “Намерений” с помощью Intent функций в ViewModel.
Вот как происходит взаимодействие между View и Model при выполнении “жестов” на View.
Вы видите, что print(«card chosen: \(card)«) превращает нашу карту Card в строку String, сообщая нам полную информацию о нашей карте Card, распечатывая все её переменные vars:

Я говорил вам, что это очень крутая “фишка”  и она прекрасно подходит для отладки.

На этой неделе Лекции закончены.
Ваше Домашнее Задание должно немного улучшить этот код.

Например, если вы заметили, то наши карты не перемешаны (not shuffled) и поэтому играть в нашу игру очень легко, так как “совпадающие” карты следуют друг за другом.
Вам нужно будет перемешать эти карты.
Вам нужно сделать карты более привлекательными, а не оставлять их такими высокими и стройными. Вы также должны размешать случайное количество пар карт, начиная с 2-х и до 6-ти пар, об этом сказано в вашем Домашнем Задании.
Каждый раз у вас должно быть случайное количество пар карт и они должны быть перемешаны.
Это ваше Домашнее Задание.
Вы видите, что большая часть вашего Домашнего Задания заключается просто в воспроизведении кода, который мы получили на данный момент.
Изменения, о которых я упомянул, сводятся к одной строке кода и не потребуют от вас много усилий, но вы должны понимать всё, что вы пишите.
Домашнее Задание должно быть выполнено к следующей неделе.
Наслаждайтесь вашим Домашним Заданием, если у вас будут вопросы, то вы знаете, как зайти на Piazza и мы будем рады ответить на них.

Для получения более подробной информации посетите наш сайт stanford.edu.
——- 104 -ая минута лекции ———

Конец Лекции 2