Лекция 1 CS193P Spring 2020 — Логистика курса и Введение в SwiftUI. Часть 2.

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

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

Код демонстрационного примера для Лекции 1 находится на Github в папке Memorize L1

——————      ПРОДОЛЖЕНИЕ КОНСПЕКТА      ————————————————- 35 -ая минута лекции ———

Представление о размере в точках points вы можете получить из того, что типичный iPhone имеет 400 — 500 points в ширину и 700 — 800 points в высоту.  iPad имеет размер 1,000 x 700 points.

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

SwiftUI помогает нам писать код, который работает на экранах любого размера, он просто адаптируется и подстраивается под любой размер экрана.  И вы увидите это в действии в нашей игре Memorize, когда у нас появятся строки и столбцы кнопок. На большом экране они будут большими. Когда мы в портретном режиме, они будут иметь один размер, а в ландшафтном режиме — другой. Мы всегда хотим писать код, который будет работать на экранах любых размеров.

Но я привел вам примерные размеры устройств в точках points для того, чтобы вы приблизительно поняли, что это такое.

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

Она потребовалась только прямоугольнику с закругленными углами RoundedRectangle, для создания Text метка была не нужна, нам не понадобилась метка типа string или ещё что-то подобное. У Text вообще нет меток. 

Но нормой как раз является наличие меток для информации, использующейся для создания. Text — это скорее исключение, когда вообще нет меток для аргументов.

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

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

Так что одна из наиболее распространенных вещей, к которым нужно привыкнуть в Swift, это то, что почти все параметры функции имеют метки. И это то, что Swift унаследовал от Objective-C. И это действительно замечательно, потому что если бы у нас не было метку cornerRadius при создании  RoundedRectangle, то чтобы 10.0 значило?

Это ширина, высота прямоугольника или радиус закругленного угла?
Что означает этот 10.0?
Но как только мы разместили метку cornerRadius

… то вдруг всё становится  понятным: “ Я создаю прямоугольник RoundedRectangle с закругленными углами и радиус закругления cornerRadius равен 10.0.”

Сейчас трудно увидеть, что у нас прямоугольник действительно с закругленными углами, потому что курсор в коде стоит точно на строке кода  с RoundedRectangle, а это приводит к появлению голубой линии, обозначающей его на экране. 

Но если я кликну где-то в стороне от этой строки кода и увеличу масштаб, то смогу отчетливо увидеть закругленный угол:

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

Но обо все по порядку. Мы вернемся к этому позже.

Следующее, что я хочу сделать, это объединить прямоугольник с закругленными углами RoundedRectangle и Text с эмоджи, потому что, как вы помните, наши карты содержат как прямоугольник с закругленными углами RoundedRectangle, так и Text с эмоджи, который расположен поверх прямоугольника:

Я собираюсь выполнить это объединение с помощью некоторого View-объединителя и его возврата как some View. Таким View-объединителем является ZStack:

ZStack — это просто структура struct, которая ведет себя как View, такая же, как и RoundedRectangle, и Text, и наш ContentView. ZStack не нуждается в аргументах при создании, за исключением единственного аргумента, который называется content и задается парой фигурных скобок:

В фигурных скобках размещается список Views, которые располагаются один поверх другого:

И, очевидно, нам не нужны returns внутри фигурных скобок.

Итак, это список: RoundedRectangle — на заднем плане, Text — впереди.
У нас в списке два элемента UI, но может быть 6 или 7 элементов, которые накладываются друг на друга, один поверх другого.

ZStack и есть то самое some View. Фактически, возвращается ZStack вместо RoundedRectangle или Text, или ещё чего-нибудь:

Но Swift АВТОМАТИЧЕСКИ распознает это и вернет ZStack в качестве some View. Он сделает это за нас :

Такой View-объединитель действительно является очень важным при создании сложных Views. На следующей неделе мы создадим свой собственный  View-объединитель и поймем, как всё это работает.

Для того, чтобы посмотреть, на что похож наш UI, нам нужно опять использовать Resume :

И всё-таки это мало похоже на карту :

Прежде всего потому, что наши карты не закрашены черным цветом, а обведены оранжевым цветом снаружи:

Кроме того, у нас есть ещё проблема, которая заключается в том, что трудно разглядеть края карты, так как они выходят за границы экрана:

Давайте решим эти две проблемы.

Сначала давайте обведем (stroke) нашу карту вместо того, чтобы закрашивать. Для этого мы вызовем функцию для RoundedRectangle, которая называется stroke () :

Мы знаем, что прямоугольник с закругленными углами RoundedRectangle ведет себя как View. Он должен вести себя как View, а иначе он не может быть размещен в ZStack также, как и этот Text.

Но RoundedRectangle также ведет себя как Shape (геометрическая фигура). Другими геометрическими фигурами Shapes являются Circle (окружность), Capsule (капсула), Paths (кривые Безье), Все они являются Shapes (геометрическими фигурами). А все Shapes ( геометрическими фигурами) можно обвести (be stroked) путем вызова функции  с именем stroke ().

Именно это и произошло с нашим прямоугольником с закругленными углами. Мы обвели по краям наш RoundedRectangle.

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

Функция stroke () — это интересная функция, которая что-то возвращает:

Угадайте, что она возвращает? Она возвращает View, точнее что-то, что ведет себя как View. И она обязана это делать, быть View, иначе мы не смогли бы поместить её в ZStack.

Так что похоже, что возвращается ОБВЕДЕННЫЙ прямоугольник с закругленными углами  RoundedRectangle.stroke (), который ведет себя как View:

У нас всё ещё два Views находится в ZStack. Одним из них является ОБВЕДЕННЫЙ прямоугольник с закругленными углами RoundedRectangle.stroke(), а другим — текст Text.

Этот шаблон, когда вызывается функция для View или геометрической фигуры Shape с тем, чтобы вернуть другое View, является очень распространенным.

Сейчас вы впервые увидели вызов функции в Swift. В действительности это очень просто. Ставите точку, затем имя функции и далее в круглых скобках указываете любые аргументы. На самом деле у функции stroke() есть аргументы, но она прекрасно работает и без них.

Без аргументов функция stroke() выполняет обводку геометрической фигуры Shape по умолчанию линией, толщиной в 1 point :

А что насчет цвета обводки прямоугольника с закругленными углами RoundedRectangle?

Сейчас цвет похож на темно-серый (Dark gray). А что, если мы хотим обводку оранжевого цвета?

Как мы можем это сделать?

Все Views или любой, кто ведет себя как View, могут вызвать функцию с именем foregroundColor с единственным аргументом:

… который представляет собой цвет Color :

Я собираюсь изменить цвет на оранжевый:

Функция foregroundColor устанавливает цвет, который используется для рисования View.

Действительно, если мы в коде «сойдем» со строки RoundedRectangle и установим курсор где-то ещё, то увидим, что цвет обводки изменился на оранжевый:

Функция foregroundColor может быть вызвана для любого View.
В частности, она может быть вызвана и для Text:

Видите? Никакой ошибки.
Но, конечно, это нам вряд ли поможет с изменением цвета Text, потому что у нас эмоджи.
Однако если я заменю эмоджи на букву “X” , то мы увидим, что у неё оранжевый цвет:

Но с нашим эмоджи вызывать функция foregroundColor для Text не имеет смысла.
Что ещё мы могли бы сделать оранжевым так это наш ZStack:

Но с нашим эмоджи вызывать функция foregroundColor для Text не имеет смысла.

Что ещё мы могли бы сделать оранжевым, так это наш ZStack:

Но что значит для ZStack иметь оранжевый цвет?

ZStack, ведь просто View, он  ведет себя как View, и он, конечно, может вызывать функцию foregroundColor, как и все другие View в этом мире.

Но для ZStack это означает нечто другое, а именно: “Скажи каждому View, находящемуся внутри тебя, что оно должно использовать функцию foregroundColor (Color.orange)

И это работает для нас нужным образом.
Посмотрите, у нас карта обводится оранжевым цветом:

И опять, если я заменю “Привидение” на буквы X, то она также будет оранжевого цвета, потому что весь ZStack имеет оранжевый цвет:

По сути, мы устанавливаем некоторую “среду” (environment), которую все View, находящиеся внутри ZStack, используют для рисования (draw).

Но для любого из этих View, мы можем переопределить (override) условия рисования..

Мы можем выполнить обводку нашего прямоугольника с закругленными углами RoundedRectangle голубым цветом:

Но внутри ZStack цвет все ещё оранжевый и если я опять заменю “Привидение” на букву X, то увижу это:

Это произошло потому, что я переопределил цвет с помощью foregroundColor(Color.blue) только для одного View, прямоугольника с закругленными углами RoundedRectangle.

Это вовсе не переопределяет цвет для буквы X.

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

И это особенно важно для ZStack, потому что мы его используем для карты, все части которой мы хотим сделать оранжевыми — и элементы лицевой стороны карты, и обратную сторону карты. Мы всё хотим сделать оранжевым, и это совершенно замечательно, что нам достаточно установить цвет сразу целиком для ZStack:

Обычно мы размещаем эти модификаторы (modifiers) для View-объединителей на отдельной строке с небольшим отступом:

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

Итак, мы достигли определённого прогресса в отображении нашей карты, но по-прежнему всё ещё трудно увидеть края карты. Нам определенно нужно сделать зазор для краёв карты.
Для этого я использую еще одну View функцию с именем padding ().
Если я использую padding () для моего Text, то я получу не просто “голый” текст с эмоджи, а эмоджи с отступами по краям:

Я могу сделать то же самое для прямоугольника с закругленными углами RoundedRectangle, так как это тоже View

Прекрасно, теперь наша карта имеет зазоры по краям.
Я даже могу использовать padding () для получения отступов для целого ZStack:

Возможно, именно это мне и нужно, потому что я не хочу задавать отступы для каждого отдельного View в ZStack. Например, только для RoundedRectangle. Потому что мы будем также создавать обратную сторону карты, которая также возможно потребует RoundedRectangle, так что я хочу, чтобы формирование отступов для карты за пределами отдельных UI элементов.

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

Теперь давайте обратим внимание на очень тонкое различие между padding () и foregroundColor(Color.orange).

Функция padding () дает отступы целому стеку ZStack. Именно сам стек  ZStack получает область с отступами и всё, что находится внутри стека ZStack, будет рисоваться внутри этой уменьшенной за счет отступов области.

В противоположность этому, функция foregroundColor(Color.orange) не имеет никакого непосредственного воздействия на сам стек ZStack, так как у ZStack нет цвета.  Она воздействует лишь на те Views, которые находятся внутри стека ZStack. Функция foregroundColor определяет лишь “среду” (Environment) рисования, которая передается вниз по иерархии.

Вот как теперь выглядит наша карта в  Preview области:

Давайте посмотрим, как наша карта будет выглядеть на симуляторе:

Интересно.
Безусловно, все работает, но карта выглядит не совсем так, как нам бы хотелось.
Это потому, что наш симулятор находится в Dark Mode.
Однако мы хотим, чтобы фон для лицевой стороны карты был белым White, независимо от того, находится ли устройство в  Dark Mode или в Light Mode. Мы не хотим иметь черное на черном.

Как нам этого добиться?

К сожалению, мы не можем послать RoundedRectangle функцию, которая бы обводила (stroke) наш прямоугольник с закругленными углами одним цветом Color, а закрашивала (fill) бы его другим цветом Color. Но для нас это — не проблема, так как у нас уже есть стек ZStack и мы можем просто создать ещё один прямоугольник с закругленными углами RoundedRectangle и закрасить (fill)  его с помощью функции fill (), вместо того, чтобы обводить (stroke) с помощью функции stroke ()

Как вы помните, этот закрашенный  прямоугольник с закругленными углами будет находится позади всех других Views в стеке ZStack.  

Каким же цветом он закрашивается? Он закрашивается тем цветом, который указан в “среде”, определяемой функцией foregroundColor(Color.orange) для стека ZStack:

Но мы хотим закрасить наш новый прямоугольник с закругленными углами RoundedRectangle белым цветом и, к счастью, у функции fill () есть необязательный аргумент и этим аргументом является цвет Color, которым она и производит закрашивание. Для нас это будет белый цвет Color.white:

Для нашего нового прямоугольника с закругленными углами RoundedRectangle мы переопределили (overrides) цвет закрашивания прямоугольника на БЕЛЫЙ вместо того цвета, который был установлен с помощью foregroundColor (Color.orange) для стека ZStack:

И получаем БЕЛЫЙ фон для нашей карты:

Если я кликну на кнопке Run и запущу симулятор, то мы увидим белый фон нашей карты и на симуляторе:

На симуляторе вы видите оранжевую границу нашей карты, но она едва-едва различима, так как они имеет ширину 1 point  и примыкает непосредственно к черному фону.
Её действительно почти не видно.
Давайте это исправим.
Я говорил вам, что у функции stroke () могут быть другие аргументы, и один из них — ширина линии обводки lineWdth. Я сделаю её равной 3:

Если я кликну на Resume кнопке, то мы увидим границу нашей карты в Light Mode:

Мы также можем запустить симулятор и посмотреть на нашу карту в Dark Mode:

Между прочим на симуляторе вы можете управлять Dark Mode и Light Mode с помощью маленькой кнопки, расположенной в самом низу:

Вы видите Interface Style? Можно включить эту опцию с помощью переключателя справа:

И перейти на Light Mode:

Теперь вернемся к нашему симулятору и увидим, что он находится в Light Mode:

Это действительно крутой способ переключаться с Dark Mode на Light Mode и наоборот.
Итак, мы достигли определенного прогресса в отображении карты, но эмоджи по-прежнему очень маленького размера.
Давайте сделаем эмоджи большего размера с помощью функции установки шрифта
font (Font.largeTitle):

Я использовал заранее определенный шрифт с именем largeTitle. Есть и другие предопределенные шрифты, например, subheadline или body, но largeTitle — это самый большой, который мне удалось обнаружить.

Если я опять кликну на Resume кнопке, то наш эмоджи станет немного больше:

Но всё-таки недостаточно большим. На следующей неделе мы будем изучать масштабирование шрифтов и то, как сделать шрифт нужного размера для нашей карты.
На данный момент, наш эмоджи пока не такого размера, который нам нужен, но все-таки чуть-чуть побольше.
Одна из причин, по которой я хочу вам показать эту функцию font (Font.largeTitle), заключается в том, что функция font (Font.largeTitle) может быть перенесена на уровень стека ZStack

В этом случае функция font (Font.largeTitle), также как и функция foregroundColor(Color.orange), устанавливает “среду” для всех Views, входящих в стек ZStack:

Но в отличие от функции foregroundColor(Color.orange), которая устанавливает цвет и которая действует на ВСЕ Views, входящих в стек ZStack, функция font (Font.largeTitle) воздействует ТОЛЬКО на ВСЕ Text, входящие в стек ZStack. Легко представить себе множество Text внутри стека ZStack, которые должны использовать один и тот же шрифт.
Так что установка единого шрифта для ВСЕХ Text внутри стека ZStack — это реально крутая возможность. 

Худо-бедно я получил карту, которая выглядит так, как я хочу.
Но пока у меня всего одна карта, а я хочу иметь множество карт.
Как я могу получить множество карт?
Я собираюсь вернуть View другого типа, другой View-объединитель (combiner View).
Он называется
ForEach. Конечно, у него есть аргументы:

 И первым аргументом является то, что можно назвать “итерабельная вещь” iteratablething, то есть как перечислять то, что находится в списке ForEach, а вторым аргументом, также как и в ZStack, является content:

Аргумент content в данном случае — это наш ZStack:

Views-объединитель ForEach делает следующее. Он проходит по всем элементам  “итерабельной вещи” iteratablething и для каждой из них создает View, представленное аргументом content. По сути ForEach —  это механизм для получения множества Views. ForEach —  это своего рода View, репликатор или View итератор, именно так можно думать о ForEach.

Что же это за “зверь” такой —  “итерабельная вещь” iteratablething?

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

Обычно это массив Array. Мы даем ForEach массив элементов, и он для каждого элемента массива создает свой View.

Но сейчас я не буду использовать массив Array, позже мы переключимся на использование массива Array, а сейчас в качестве  “итерабельная вещь” iteratablething я буду использовать диапазон Range. В моем конкретном примере будет диапазон целых чисел от 0 до 4, исключая саму 4:

Вот как выглядит Swift синтаксис для задания диапазона Range. Этот диапазон простирается от некоторой нижней границы (0) до некоторой верхней границы (4) и НЕ ВКЛЮЧАЕТ верхнюю границу.

Если я задам диапазон помощью 3-х точек, а не с помощью 2-х точек и знака “меньше”, то диапазон будет ВКЛЮЧАТЬ верхнюю границу:

Но я не хочу, чтобы верхняя граница была включена, я хочу пройти только через 0, 1, 2 и 3. И для каждого из этих значений будет создан ZStack:

По существу ForEach — это “повторитель”.
Есть одна очень крутая вещь, касающаяся ForEach.
У него тот же самый аргумент content, что и у ZStack:

Но в случае с ZStack мы имеем одно View, представленное аргументом content, а точнее фигурными скобками, а в случае с ForEach — это целый список View, который также представляется фигурными скобками. И это отличие выражается в том, что в фигурных скобках для ForEach присутствует index in :

По сути indexэто переменная итерации. Вначале она равна 0, затем 1, затем 2 и, наконец, 3. Итого, у нас 4 итерации.

И что интересно относительно ForEach, то мы видим в Previews области совершенно отдельные Previews для каждой итерации: 

И это потому, что ForEach — это вовсе не о том, как располагаются Views, это НЕ похоже на ZStack. ZStack занимается тем, что позиционирует Views на экране один поверх другого, начиная с заднего плана и двигаясь к переднему плану. 

ForEach вообще не заботится о местоположении Views, он их генерирует. Поэтому если мы запустим симулятор, то Views, сгенерированные ForEach, кое как разместятся на экране, так как симулятор не умеет симулировать множество iPhones. Но в Preview области у нас больше возможностей для симуляции того, что происходит на самом деле при создании этих 4-х Views. Так что Preview область показывает каждое из этих 4-х Views, созданных ForEach, на отдельном устройстве. 

Что невероятно удобно при отладке, так как вы четко можете сказать, какое из них как выглядит. У нас же все Views абсолютно одинаковые, так что для нас это не очень интересно.
Но я хочу, чтобы все 4 Views появились на одном экране одновременно. И для этого нам нужен другой стек. Нам не нужен стек, который размещает Views один поверх другого и который не дает нам увидеть ничего, что расположено на заднем плане.
Нам действительно нужен другой стек, а именно горизонтальный стек, и мы возвращаем HStack, который также, как стек ZStack, имеет аргумент content:

И внутри content мы разместим то, что мы хотим, чтобы HStack организовывал для нас :

HStack вместо того, чтобы располагать Views один поверх другого, начиная с заднего плана и двигаясь к переднему плану, располагает Views по горизонтали, двигаясь в направлении слева направо, и именно это отражает буква H (Horizontal) в названии стека HStack:

Видите? Все Views расположились по горизонтали слева направо.

Я говорил вам, что Swift никоим образом не заставляет вас печатать то, что вы можете и не печатать. И в данном случае я убираю ключевое слово return, потому что хотите — верьте, хотите — нет, но возвращается одно единственное View, которое и является значением этой вычисляемой переменной body, имеющей ТИП some View:

Это ЕДИНСТВЕННОЕ View.
Да, это горизонтальный стек HStack для всех Views, созданных с помощью ForEach, каждый из которых представляет собой стек ZStack с отступами, определяемыми padding() и имеющими цвет foregroundColor и шрифт font. Но это ЕДИНСТВЕННОЕ View, и в Swift мы имеем право вернуть его без ключевого слова return.
Конечно, HStack — это View-объединитель (combiner), объединяющий множество Views.
И ForEach — это View-объединитель (combiner), и ZStack — это View-объединитель. Но все эти множественные Views находятся внутри одного ЕДИНСТВЕННОГО HStack.

Заметим, что если HStack видит ForEach, то понимает, что он будет иметь дело со списком Views. Он вообще готов увидеть у себя внутри список Views.
Давайте добавим в HStack текст Text(«hello»):

Мы видим, что сначала HStack расположил текст Text(«hello»), а затем распознал список Views из ForEach как отдельные ZStack и расположил их отдельно по горизонтали. Это говорит о том, что все эти стеки — очень “сообразительные” ребята, они знают, что нужно делать, если внутри них находится ForEach.

Другая вещь, на которую следует обратить внимание, это горизонтальные зазоры между Views в HStack

Это обеспечивается padding () ?
Нет.
Функция padding () обеспечивает внешние отступы для HStack, потому функция padding () как раз вызывается для самого HStack.
Маленькие горизонтальные зазоры между картами создаются непосредственно самим HStack, и у него есть аргумент spacing, позволяющий ему регулировать величину этих зазоров.
Я установил аргумент spacing равным 0:

Я могу установить его равным 50:

Или оставлю его неопределенным:

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

То же самое касается padding (). Мы ничего не определили в padding (), хотя могли бы сделать отступ сверху в 100 points с помощью padding (.top, 100):

Или отступ со всех сторон в 10 points с помощью padding (10):

Но в большинстве случаев, если это возможно, то мы делаем стандартные отступы с помощью padding () без аргументов:

То есть в большинстве случаев у нас стандартные отступы padding () и зазоры spacing.

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

Помните? Мы уже убрали return перед HStack.
Есть еще одна вещь, которую мы можем убрать.
В Swift, если последним аргументом функции или конструктора является выражение в фигурных скобках, то хотите — верьте, хотите — нет, но вы можете избавиться от метки этого аргумента и вынести выражение в фигурных скобках за пределы вызова этой функции.
Например, при создании ForEach используется два аргумента, причем вторым аргументом как раз является выражение в фигурных скобках:

Мы избавляемся от метки content и переносим выражение в фигурных скобках за пределы круглых скобок:

Это кажется невероятным, но посмотрите, что произойдет с нашим кодом, если я проделаю эту операцию сначала с ZStack, у кого выражение в фигурных скобках является последним аргументом:

И с HStack, у которого также выражение в фигурных скобках является последним аргументом:

Код стал намного понятнее, у вас нет этих лишних меток content.
Но мы можем пойти дальше.
Если у ZStack и HStack нет аргументов и есть выражение в фигурных скобках, то вы вообще можете убрать эти круглые скобки :

Наш код стал значительно более понятным.

Вообще в большинстве случаев мы будем использовать ZStack и HStack без круглых скобок и без аргументов. Но если мне понадобится установить какие-то зазоры, я смогу очень легко это сделать с помощью аргумента spacing:

Аргумент spacing теперь является первым аргументом HStack, а выражение в фигурных скобках по-прежнему останется последним аргументом HStack и находится за пределами вызова конструктора HStack. Это делает код немного более понятным.

Наш код, отображающий 4 карты уже выглядит хорошо:

Но мы можем ещё его улучшить.
ZStack по сути представляет одну нашу карту:

Было бы здорово создать новую структуру struct с именем CardView, которая также вела бы себя как View.
А что это означает?
Это означает то, что у неё есть переменная var body, у которой ТИП some View:

Вычисляемая переменная var body должна вернуть some View, и этим some View будет ZStack:

Мы убрали ZStack из ContentView и вставили его в CardView.
А в ContentView вместо ZStack я должен создать карту с помощью CardView():

Именно так мы создаем отдельные Views, перенося в них часть кода, а затем инкапсулируем изъятый код в виде создания этих Views.
Это делает наш код достаточно простым и понятным. Кто-нибудь теперь взглянет на ContentView и легко поймет, что здесь происходит:

То же самое можно сказать и о структуре struct CardView, которая выглядит очень просто. 

И, наконец, последнее, что мы сделаем на этой  Лекции — это создание ЛИЦЕВОЙ и ОБРАТНОЙ сторон карты. Сейчас перед нами только ЛИЦЕВАЯ сторона карты. Я не могу в данный момент показать ОБРАТНУЮ сторону карты:

Как мы нарисуем обратную сторону карты?

У нас уже есть стек ZStack с ЛИЦЕВОЙ стороной карты. В этот же ZStack мы поместим и ОБРАТНУЮ сторону карты, которая будет просто закрашенным прямоугольником с закругленными углами RoundedRectangle:

Мы скопировали и добавили еще одну строку с RoundedRectangle, которая и будет ОБРАТНОЙ стороной карты. Очевидно, что она будет закрашиваться не белым цветом, а цветом, установленным в “среде”, то есть тем цветом, который задается для HStack выражением foregroundColor(Color.orange).
Заметьте, что цвет, указанный в “среде” применяется не только к Views, объединяемым горизонтальным стеком HStack, но ко всем Views, создаваемым с помощью ForEach, то есть к CardView и далее вниз по иерархии к ZStack и RoundedRectangle.

Я на секунду закомментирую код, относящийся к ЛИЦЕВОЙ стороне карты, кликну на кнопке Resume и мы увидим ОБРАТНУЮ сторону карт:

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

Но в действительности мы хотим видеть либо ЛИЦЕВУЮ сторону карты, либо ОБРАТНУЮ сторону в зависимости от некоторых условий. Другими словами либо карту, лежащую ЛИЦЕВОЙ стороной вверх (face up) или ЛИЦЕВОЙ стороной вниз (face down). Для этого нам понадобится синтаксическая конструкция типа  if-then.

К счастью у нас есть стек ZStack, в котором разрешается размещать любой список Views, а также использовать условные предложения if isFaceUp, то мы показываем ЛИЦЕВУЮ сторону карты, в противном случае — ОБРАТНУЮ :

У нас есть ошибки, связанные с тем, что я никак не определил, что собой представляет isFaceUp.
Это должна быть переменная var, обычная хранимая в памяти переменная var isFaceUp без всяких там фигурных скобок:

Мы видим, что ошибки исчезли в структуре struct CardView, она прекрасно внутренне логически согласована. Если булевская переменная isFaceUp равна true, то показывается ЛИЦЕВАЯ сторона карты, а если isFaceUp равна false, то ОБРАТНАЯ сторона карты.
Но в структуре struct ContentView появилась ошибка, вызванная как раз тем, что  в структуре struct CardView появилась новая булевская переменная isFaceUp:

Нам сообщают, что пропущен аргумент ‘isFaceUp’.
Что это означает?
Я говорил вам раньше, что в Swift все переменные должны быть строго типизированными, то есть иметь начальные значения.
Так что мы можем установить начальное значение переменной isFaceUp, например, равным false:

Это привело к исчезновению ошибки и к тому, что false значение для переменной isFaceUp, заработало и у нас показаны карты с ОБРАТНОЙ стороной, то карты, которые лежат ЛИЦЕВОЙ стороной ВНИЗ.

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

Если я изменю значение переменной isFaceUp на true, то мы увидим ЛИЦЕВУЮ сторону карт, то есть карты лежат ЛИЦЕВОЙ стороной ВВЕРХ:

Но это лишь один из способов установить значение для переменной isFaceUp, и мы чаще всего так и будем делать, если это имеет смысл.
Но если мы не установим значение по умолчанию переменной isFaceUp, то в ContentView опять появится ошибка:

 … которая говорит о том, что при попытке создания CardView переменная isFaceUp оказалась не инициализированной, но она должна быть инициализированы и эта задача возлагается вас. И я хочу вам продемонстрировать одну крутую возможность сделать это.

Вы кликаете на маленьком красном кружочке с ошибкой:

И в большинстве случаев появляется одна или несколько опций для исправления этой ошибки:

Мы кликаем на кнопке Fix, и компилятор сам исправляет ошибку вместо нас. Он не всегда исправляет ошибку так, как нам нужно, а иногда и вообще опция Fix не появляется. Но в нашем случае опция Fix работает:

Она добавляет аргумент isFaceUp в конструктор CardView с тем, чтобы мы установили значение булевской переменной isFaceUp.
Если я укажу значение false для аргумента isFaceUp, то инициализируется  переменная var isFaceUp в структуре struct CardView, и после того, как кликнем на кнопке Resume, мы увидим карты, лежащие ЛИЦЕВОЙ стороной ВНИЗ:

Надеюсь, этот шаблон конструирования CardView, нам уже знаком. Мы использовали его при создании прямоугольника с закругленными углами RoundedRectangle:

Мы использовали конструктор RoundedRectangle с аргументом cornerRadius, который нас заставили установить, так что, возможно, у RoundedRectangle есть переменная var с именем cornerRadius. Хотя вовсе необязательно, так как есть и другой способ передачи значений.
Но в любом случае наличие неинициализированных переменных vars в некоторой структуре struct заставляет нас устанавливать им значения при конструировании этих структур.

На этом первая Лекция завершена.

Следующую Лекцию мы начнем со слайдов и некоторых концептуальных вещей. Затем мы вернемся к нашему демонстрационному примеру.
Обычно я рассказываю более подробно о том, что будет на следующей Лекции, но в данном случае я сразу выложим две Лекции, так что вы можете прямо сейчас её посмотреть.

Более подробная информация на stanford.edu.

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

Конец Лекции 1.