Лекция 4. Применение MVVM. CS193P Spring 2023.

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

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

Итак, вся эта неделя будет посвящена этой картинке, которую я показал вам на прошлой Лекции.

Она относится к нашей игре Memorize таким образом, что мы собираемся использовать её для построения ЛОГИКИ игры, а к концу Среды мы уже на самом деле будет играть в игру Memorize.
Давайте просто вернемся к нашему коду, к тому места, на котором мы остановились в прошлый раз.

Позвольте мне дать некоторый обзор того, где мы находимся на данный момент, по сравнению с представленной выше картинкой. То, что вы видите здесь, этот класс class : EmojiMemoryGame, наша ViewModel. На первой и нашей основной картинке это зеленая вещь:


А это наша Model (Модель). На первой картинке это синяя штука внизу.

В настоящее время мы уже создали наше View, оно называется ContentView:

Мы собираемся изменить его на что-то другое, но кусочек его мы уже создали. Обратите также внимание, что когда я кликнул на файле MemorizeGame, то увидел, что там находится структура struct с именем MemoryGame. Давайте просто переименуем файл, вам разрешено выделять имена файлов и …

… изменять их:

Если вы хотите переименовать актуальную структуру struct, тогда вам нужно переименовывать немного больше вещей, поскольку вы даете новое имя структуре struct, файлу и другим вещам, ссылающимся на эту структуру struct. На самом деле я собираюсь показать вам, как это сделать, когда мы будем переименовывать структуру struct ContentView в EmojiMemoryGameView, это имя значительно лучше слишком обобщенного имени ContentView. Я покажу вам это через секунду. Если вы вернетесь в ваш ViewModel, вы заметите, что там мы создали переменную var для нашей модели model:

На первой и основной картинке наш ViewModel имеет полную возможность подключения к Model (Модели). Он может «поговорить» с Model (Моделью) обо всём, что ему нужно, потому что вся его работа именно в этом и состоит: чтобы понять Model (Модель), «поговорить» с ней, интерпретировать её данные и представить их View действительно наилучшим способом. 

Иногда мне нравится называть ViewModel “дворецким” (butler) Views. Вы когда-нибудь смотрели «Аббатство Даунтон» или что-то подобное? Там вы видите этих старых “дворецких”, они накрывают на стол, чтобы вы вкусно пообедали, и они устраивают все ваши дела. Именно это и делает ViewModel, беря все, что есть в Model (Модели), и организуя это наилучшим образом, чтобы у View был такой красивый и простой код, что всё действительно понятно.
В этом и состоит роль ViewModel
Конечно, у ViewModel  есть переменная var model прямо в коде:

… и мы собираемся перейти к нашему ContentView и добавить переменную var в наш View, которую я назову viewModel, это плохое название, но ТИП переменной var viewModel будет EmojiMemoryGame:

Другими словами, у нас будет переменная var viewModel в нашем View, которая указывает на нашу ViewModel. Очевидно, мы должны иметь возможность попросить нашего “дворецкого” сделать для нас определенные вещи, Ask Jeeves о том, что нам нужно, поэтому нам необходима переменная var viewModel, которая на него указывает.

Кстати, кажется, я говорил это раньше и собираюсь сказать еще пять раз: “Я назвал эту переменную viewModel, но вам НЕ следует называть её var viewModel, вам следовало бы назвать её var gameMemory или чем-то в этом роде значимым”.
Я назвал эту переменную var viewModel, a другую переменную — var model, и сделал я это умышленно, чтобы вы четко понимали “кто есть кто”, но в реальной жизни не стоит называть их так. Мною это было сделано исключительно для понимание того, что здесь происходит.
Если вы посмотрите на нашу картинку, то обратите внимание, что путь от Model к View через ViewModel настроен с помощью этих двух переменных — var model и var viewModel, которые указывают на Model и на ViewModel
:

Но обратите внимание, что в моей ViewModel нет и НИКОГДА не будет переменной var, указывающей на View.
Единственный способ, каким ViewModel взаимодействует с View, — это “что-то изменилось” (“something changed”). Если ViewModel просто говорит: «О, что-то во мне изменилось«, то затем наступает ответственность View “вытянуть” из этого “что-то изменилось”  то, что интересно ему. Это происходит потому, что это реактивная реакция “существа”, у которого нет состояния, оно является Stateless (Без состояния), но которое умеет “вытянуть” (pull) свои данные и посмотреть, изменилось ли там что-то, и если да, то перерисовывать только то, что нужно изменить. Это все часть механизма View, поэтому у вас в ViewModel НИКОГДА не будет переменной var, указывающей на View.

Также, конечно, у вас никогда не будет в Model переменной var, указывающей на ViewModel, потому что Model является UI-независимой, a ViewModel — это часть UI. Так что Model просто совершенно запрещено “разговаривать” или знать что-либо о ViewModel или о View, да в этом и нет никакого смысла, поскольку Model абсолютно независима от UI, a эти две вещи являются частью UI.

Вот какие отношения между всеми этими вещами:  Model, View, ViewModel.

Собственно, позвольте мне сказать еще кое-что об отношениях между этими вещами, поскольку в слайдах я говорил о “частичном разделении”, о “полном разделении” и о том, как разместить вашу Model в @State, что похоже на “отсутствие всякого разделения”. 
Помните? 
То, что вы видите в этом коде, — это “частичное разделение”:


Почему так?
Потому в моем View есть эта переменная var viewModel, которая указывает на экземпляр одной из ViewModel, a там есть эта переменная var model. И мне ничто не мешает в моём View написать viewModel.model и “поговорить” напрямую с моей Model.

Управление доступом (access control)

Но я могу предотвратить это, если я хочу иметь “полном разделении”, я могу вставить ключевое слово private:

Если я сделаю переменную var model private, это означает, что единственный код, который может использовать эту переменную или “видеть” её, это код внутри класса class EmojiMemoryGame.
Это private для этого класса class.
Если я так сделаю, то как View получит доступ к картам cards, чтобы в них играть?
В данном случае ответственность за это лежит на ViewModel. Например, можно предоставить вычисляемую переменную var cards, которая возвращает массив Array<MemoryGame<String>.Card> из Model (Модели) model:

Может показаться, что совершенно нелепо иметь эту однострочную вычисляемую переменную var, которая возвращает model.cards, но на самом деле это защищает Model от View. View должен пройти через ViewModel, чтобы добраться до таких вещей, и действительно, наверное, это выглядит немного нелепо в нашем случае, так как наша Model очень проста, в ней имеются только две вещи: cards и функция func choose, вот и вся Model.

Именно поэтому я бы сказал, что возможно, для нашей Modelчастичное разделение” было бы вполне приемлемым, мы могли бы оставить нашу переменную var model public. Но если мы решили, что нам нужно “полное разделение” и мы сделали переменную var model private, то мы не только должны сделать так, чтобы View увидел наши карты cards, нам также придется предоставим View и функцию выбора карты func choose (card: MemoryGame<String>.Card), которая возвращает model.choose(card: card):

Придется обеспечить все это. 
Кстати, эту функцию мы бы назвали Намерением (Intent). Помните, я говорил о Намерении (Intent), у ViewModel есть Намерение (Intent); в нашем случае это Намерение (Intent) пользователя выбрать карту. Мы увидим больше информации о функциях Намерения (Intent); мы собираемся добавить еще одну функцию Намерения (Intent). 
Так что на данный момент у нас “полное разделение”, private var model, только ViewModel может видеть это, именно ViewModel делает public все функции func, переменные var и хочет это сделать. 

Теперь давайте также поговорим немного о защите со стороны Model,  потому что эта переменная cards, очевидно, что другому коду требуется увидеть эти карты. Если я не вижу карт cards, я не знаю, какие из них лежат “лицом” вверх, я не буду знать, какие из них совпадают, и прочее. 

Однако не хотелось бы, чтобы кто-то зашел в мою Model и начал изменять мои карты, например, переворачивать их туда-сюда и прочее. В игре Memorize есть правила, когда карты переворачиваются, что происходит с картами, когда они совпадают, и все это будет сделано внутри этой функции func choose. Мне совсем не нужно, чтобы какой-то другой код добирался до моих карт var cards.
Есть еще один способ защиты, при котором я могу разместить перед переменной var cards немного другие ключевые слова private (set):

Это как private, но означает, что только установка этой переменной var является private, a просмотр этой переменной разрешен всем людям. И это всё, что наша Model может предложить по отношению к var cards, потому что нам не нужен кто-то, кто будет вмешиваться в ЛОГИКУ нашей игры Memorize, переворачивая карты извне. Единственное, что мы можем делать с картами извне — это смотреть на них, поэтому для нас эта переменная является private (set).

Размещение private и private (set) на различных вещах в коде называется управлением доступом (access control). 

Есть еще и другие способы управления доступом (access control), которые имеют отношение к public библиотекам и всему с этим связанному, но только private и private (set) — это всё, с чем мы будем иметь дело на самом деле на этом курсе, и это 99% доступа, который вы будете использовать, пока не станете автором фреймворков или что-то в этом роде.
Я собираюсь предложить вам следующее правило: когда вы пишете код в реальном мире, то начинайте с private, если только не знаете точно, что хотите чтобы это было public
Иногда вы сразу понимаете, что не хотите сделать это private, как, например, ViewModel определенно хочет, чтобы карты cards были общедоступными, в том числе для View. Так что нет никаких вопросов, переменная var cards в ViewModel НЕ может быть private:

Но любые другие переменные var или вещи, которые у вас есть, вы склоняетесь сделать private, а затем принимаете решение: «Я собираюсь позволить другому коду вызывать это?”. Если вы приняли решение позволить другому коду вызывать это, то помните, что однажды сделав это, вы практически оказываетесь “запертым” в этом решении, потому что человек, написавший другой код, возможно, это не вы; это может быть кто-то другой из вашей команды или кто-то из другой команды вашей компании, как только они начнут вызывать эту функцию или переменную, вы должны будете оставить это НЕ private. Вы уже не можете это отменить и вернуться к private.
Это своего рода размышления по поводу того, делать ли что-то private или нет.
Итак, это управлением доступом (access control).

“Внешние” и “внутренние“ имена параметров функций

Кстати, пока мы здесь, вы видите эту функцию func choose(card: MemoryGame<String>.Card)? В вашем Задании на чтение в самом конце есть небольшой документ, в котором рассказывается о том, как выбирать имена параметров для функций, и там определенно сказано, что в случае с этой функцией нам нужно иметь только внутреннее имя параметра и совсем не нужно иметь внешнее имя. Другими словами, никаких имен аргументов на стороне вызывающего абонента. 
Так как же этого можно добиться? На самом деле это невероятно просто: вы ставите символ “подчеркивания” “_“ на месте “внешнего” имени:

Итак, в нашем случае “внешнее” имя — это символ “подчеркивания” “_“,нижняя черта, которая означает отсутствие “внешнего” имени. Поэтому, когда люди вызывают эту функцию, то просто напишут choose ( и далее следует аргумент без всякого имени, a затем закрывающая круглая скобка ). Они НЕ напишут choose (card:).
Зачем мы это делаем? Это совершенно избыточно для ТИПа Card. Понятно, что это карта; вам не нужно писать, что это карта card.
Когда у нас появятся эти имена —  “внешние” и внутренние”? 
Существует множество причин для этого, и этот документ в вашем Задании на чение расскажет вам об этом, но я скажу вам две основные причины появления имен параметров функций. 

Одна причина состоит в том, что если ТИП аргумента строка String или целое число Int или что-то в этом роде, когда непонятно что это такое, но это НЕ специфический ТИП как карта MemoryGame<String>.Card, то требуется внешнее имя для параметра.

Вторая причина: если наличие “внешнего” имени сделает код читабельным. Мы уже видели это на примере функции move, которая подразумевает смещение offset, то есть на сколько нужно переместиться, и если бы вы написали move (offset:), тогда это читалось бы “переместить смещение”. Не очень-то благозвучно, но если вы напишите move (by: offset), то это читается лучше “перемещение на”. 

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

Да, есть еще одна причина, как мы видели с функцией Image, она может принимайте разные аргументы. Например, Image(systemName:) означает, что нужно использовать системные изображения; Image(named:) означает, что нужно использовать именованные изображения.
Это разные вещи. Но в обоих случаях вы передаете строку String и вы должны описать, что это за строка String.

Мы можем сделать тоже самое в нашей игре MemoryGame, то есть написать 
 func choose(_ card: Card):

Тогда и в EmojiMemoryGame нам не нужно имя аргумента:

Довольно круто. Но это немного в сторону от нашей основной темы. 

Инициализаторы init

Так случилось, что у нас постоянно присутствует ошибка “Class 'EmojiMemoryGame' has no initializers” (“У класса 'EmojiMemoryGame' нет инициализаторов”).

Что это значит? Почему мы получили такую ошибку? 

Я говорил вам, что классы class получают “бесплатный” инициализатор init. Мы уже знаем, что структуры struct получают хорошие “бесплатные” инициализаторы, как у нашей структуры struct CardView был сначала этот “бесплатный” инициализатор, который инициализировал и content, и isFaceUp, помните? А потом мы дали isFaceUp значение по умолчанию, и тогда isFaceUp можно было вообще не задавать при инициализации CardView, но мы могли бы это сделать. Это действительно крутая “фишка”. Мы никогда не писали инициализаторов init для нашего CardView.
Классы class делают то же самое, но их “бесплатные” инициализаторы init не имеют аргументов, поэтому они работают только в том случае, если все ваши переменные var имеют значения по умолчанию, а у нас есть переменная var model, которая не имеет значения по умолчанию.
  

Вот почему нам сообщают: “Я не могу дать вам “бесплатный” инициализатор, потому что у вас есть НЕинициализированная переменная, и у вас НЕТ инициализаторов. Пожалуйста, дайте мне хотя бы один инициализатор«. 

Я мог дать EmojiMemoryGame инициализатор init, и мы увидим через секунду как инициализируется здесь переменная var model, но вместо этого я попробую дать model некоторое значение по умолчанию, например, MemoryGame<String>( ) без аргументов? Возможно, я смогу это сделать?

Нет, потому что “Missing argument for parameter ‘cards’ in call” (“Отсутствует аргумент для параметра cards”). 

Хорошо, MemoryGame() — это структура struct, это “бесплатный” инициализатор init, позволяющий мне инициализировать все переменные var, и если я вернусь в мою Model MemoryGame, то обнаружу, что в моей Model есть НЕинициализированная переменная var cards, это массив карт cards: Array<Card>:

Итак, здесь говорится:” О, это структура, хорошо, без проблем, я позволю тебе инициализироваться, но ты должна предоставить мне эти карты cards”. Другими словами, компилятор хочет, чтобы я написал что-то вроде (cards: something), но это абсолютно лишено всякого смысла:

У нас есть Model игры MemoryGame, и она знает, как играть в эту игру на “запоминание”; она знает, как создать карты cards. Кроме того, это не сработает, потому что в данный момент переменная var cards является private (set). Так что единственный, кто может создать карты cards во внутреннем массиве, это игра MemoryGame, причем она может сделать это в любом месте своего кода, так что просто нет смысла делать карты cards аргументом инициализатора init.
И здесь на помощь приходят инициализаторы. Что имеет смысл при создании Model?
Что ж, я утверждаю, что имеет смысл создание Model с помощью числа пар карт numberOfPairsOfCards, которых, например, 4, четыре пары карт. 

В моей Model нет такой переменной var numberOfPairsOfCards, но это необходимая информация для создания моей Model. Давайте перейдем к нашей Model и создадим инициализатор init:

Итак, это функция. Вы не пишите func, вы просто пишите init, и у вас могут быть любые аргументы, которые вам нужны, например, numberOfPairsOfCards, который является Int, и у вас может быть несколько инициализаторов init, все с разными аргументами. Любой, кто пытается создать игру MemoryGame, может вызвать любой из ваших инициализаторов init, и ваша задача внутри любого инициализатора init — инициализировать все ваши переменные var.
Некоторые из этих переменных var могут быть установлены по умолчанию, так что с ними ничего делать не надо, но, как в нашем случае, у нас есть эти карты cards, которые нужно инициализировать.
Я начну с того, что создам карты cards в виде пустого массива Array<Card>( ):

Массив Array представляет собой структуру struct и получает “бесплатную” инициализацию, которой не нужно ничего, никаких аргументов.
Так что БУМ! Массив cards создан.
Мы могли бы использовать и альтернативное обозначение массива [Card]( ), это совершенно законно:

Хотите — верьте, хотите — нет, но вы также можете использовать для пустого массива нотацию [ ]:

… и это шокирует людей поначалу, но на самом деле это имеет смысл, потому что с помощью этой записи мы создаем массив литералов. Мы делали это в нашем UI, когда у нас были эмодзи (смайлики) emojis:

Видите? Квадратная скобка открывается, затем в нашем случае мы перечислили несколько строк, и квадратная скобка закрывается.
В нашей Model, struct MemoryGame у нас также есть открывающаяся квадратная скобка, затем ничего, то есть пустой массив, и закрывающаяся квадратная скобка [ ], это означает пустой массив, а с точки зрения ТИПа Swift осуществляет “вывод ТИПа” из контекста (type inference), он знает, что cards имеет ТИП Array<Card>. Итак, он знает, что это пустой массив карт, именно то, что я хочу, и именно так мы и поступили бы.
Кстати, теперь мне нужно к этому массиву cards добавьте NumberOfPairsOfCards * 2 карт:

Просто я пытаюсь инициализировать себя и мне пора разместить мои карты cards

Кстати, позвольте мне занять немного вашего времени. Я решил вверху нашего экрана размещать то, о чем мы будем говорить. Мы говорили об управлении доступом (access control), сейчас мы говорим об инициализации (init), мы находимся в процессе изучения инициализации, но теперь мы переходим к следующей теме  — к циклу for, потому что сейчас нам нужен цикл for

Цикл for

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

Это единственный способ, каким вы можете выполнить цикл for в Swift. Я покажу вам, как выглядит цикл for на C, чтобы вы могли сравнить их:

Вот как это будет выглядеть на C, и похоже до версии Swift 3 вы могли это использовать и на Swift. Сейчас мы уже почти на Swift 6, так что больше я не могу это использовать. Но я действительно не хочу, чтобы вы думали о том, что как будто цикл for на C эквивалентен циклу for на Swift. Вместо этого я хочу, чтобы вы подумали о том, что в реальности происходит в цикле for на Swift, а именно о том, что, конечно, есть переменная итерации pairIndex, есть то, что можно повторять, а не просто диапазон Range типа 0..<numberOfPairsOfCards, но это может быть массив Array или любая коллекция Collection, которую можно перебирать по порядку.

Поэтому, очевидно, мы часто используем цикле for, чтобы перебирать вещи. Синтаксис циклу for на Swift —  это for <управляющая переменная> in <итерируемая вещь>:

Итак, мы собираемся пройтись по количеству пар карт numberOfPairsOfCards и что-то сделать.
Что мы будем делать? Мы просто добавим карту с помощью функции cards.append(). Функция append() для массива Array просто добавляет что-то в массив, и я собираюсь добавить карту Card. Вот моя карта Card, помните нашу структуру struct Card?
Я буду использовать “бесплатный” инициализатор, который я получаю, потому что Card —  это структура struct:

Мы начнем с того, что мои карты будут лежать “лицом” вниз и у нас нет “совпавших” карт:

У нас здесь есть CardContrent, мы еще будем говорить об этом. И, конечно, мне нужны две такие карты:

Это игра “на совпадение”, в которой мы пытаемся найти совпадающие карты, поэтому нам нужны пары одинаковых карт, которые могут совпадать. 
Всем понятно, что я там сделал?
Что это за предупреждение «Immutable  value 'pairIndex' was never used» («Неизменяемое значение 'pairIndex' (это итерационная переменная) никогда не использовалось»?
Нам предлагают замену итерационная переменной pairIndex символом “подчеркивания” “_”. 

Именно это мы и хотели бы сделать, потому что мы не используем pairIndex, но все еще хотим повторить это много раз, при этом нас не волнует, какое значение принимает pairIndex в каждой итерации, мы не используем его в этом коде. 
Поэтому мы используем символ “подчеркивания” “_”:

Вы получаете в Swift сообщение относительно использования символа “подчеркивания” “_”,  когда вы можете ничего не размещать на этом месте. Просто поставьте там символ “подчеркивания” “_”.
Мы уже делали это в функции func choose (_ card: Card), когда нам не нужно имя аргумента при вызове этой функции. В нашем случае  “_” означает, что мне все равно, какое значение принимает переменная итерации, потому что я не собираюсь на неё смотреть и использовать.
Этот код интересен тем, что вы видите здесь isFaceUp и isMatched, когда я создаю карту Card, обе эти переменные равны false, что означает, что при создании карты всегда лежат “лицом” вниз и они всегда будет “не совпавшим”. Мы должны начать игру с не перевернутых карт. Это как раз тот очевидный случай, когда  по умолчанию переменным isFaceUp и isMatched нужно присвоить значение false и убрать их при инициализации Card:

То же самое мы сделали в CardView, верно? Это очевидная вещь.
Пока мы здесь, взгляните на CardView. Я не буду указывать здесь ТИП Bool, потому что Swift может “вывести ТИП” из контекста, так как значения false и true могут принимать только Bool переменные:

Я сделаю еще одну интересную вещь здесь. Я собираюсь сделать содержимое content константой let, потому что если бы у меня была карта для игры “на запоминание”, в которой содержимое карты меняется по ходу игры, то это была бы очень сложная игра. Так что давайте пока просто предположим, что содержимое наших карт остается постоянным на протяжении всей игры. Поэтому у нас будет let content, a НЕ var content:

Но isfaceUp и isMatched являются переменными var, потому что, когда ты кликаете (Tap) на карте, она переворачивается. Иногда карты совпадают и isMatched также меняется. Так что им суждено быть переменными var. Вот о чем я говорил, здесь такая ЯВНАЯ изменчивость. Мы хотим показать, что это может меняться. 

. . . . . . . . . .

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

  • Инициализация структуры с Generic ТИПом CardContent
  • Функции как ТИПы
  • Синтаксис замыкания (closure)
  • static переменные vars и static функции funcs
  • Семантическое переименование (Rename )
  • Использование ViewModel в View
  • Инициализация init
  • Увеличение размера эмодзи (смайлика) на игровой карте
  • Перетасовка карт shuffle()
  • mutating
  • Печать print структуры struct на консоли
  • Реактивный UI: ObservableObject
  • Реактивный UI: @Published и @ObservedObject
  • Реактивный UI: @StateObject

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

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

P.S.  iOS 17 Реактивный UI: Observation

Примечание переводчика. 
Начиная с iOS 17, iPadOS 17, macOS 14, tvOS 17 и watchOS 10, SwiftUI предлагает  для Swift специальный фреймворк Observation, реализующий паттерн наблюдателя. 
Использование Observation обеспечивает вашему приложению следующие преимущества:

  • Отслеживание Optional значений и коллекций Collection объектов, что невозможно при использовании ObservableObject.
  • Использование существующих примитивов потока данных, таких как State и Environment, вместо основанных на объектах эквивалентов, таких как StateObject и EnvironmentObject.
  • Обновление Views на основе изменений наблюдаемых свойств, которые считывает body конкретных View, а не любых изменений свойств, происходящих с наблюдаемым объектом, что может повысить производительность вашего приложения.

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

Для замены существующего исходного кода, который полагается на ObservableObject, на код, использующий макрос Observable(), единственная вещь, которую вам необходимо сделать, это пометить класс class, который вы хотите сделать наблюдаемым (observable ), новым Swift макросом @Observable.

Конечно, убираем всё, что относится к предыдущему фреймворку Combine: соответствие протоколу ObservableObject и @Published:

Теперь класс class EmojiMemoryGame стал “наблюдаемым” для SwiftUI Views:

После того, как у нас есть наблюдаемый объект, мы должны решить, кому принадлежат его данные. В зависимости от данных они могут принадлежать либо конкретному View, либо всему приложению. Независимо от того, кому принадлежат данные, вы можете создать их одинаково, используя “обертку” свойства @State.

В нашем случае данные принадлежать всему приложению и мы передаем данные с помощью инициализатора:

Это наиболее простой способ передачи данных View, в котором эти данные определяются как let или var:

Вот и всё. Кликаем дважды на кнопке Shuffle: