Лекция 2. Ещё больше SwiftUI. CS193P Spring 2023.

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

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

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

Режим Выбора (Selection)  в Preview


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

Помните, я говорил вам, что вы можете войти в режим Выбора (Selection) внизу вашего PreView.
В режиме Выбора (Selection) вы сможете выбирать элементы вашего UI или даже несколько элементов одновременно.

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

И еще одна вещь, которую следует отметить по этому поводу. Я не думаю, что я упомянул о том, что этот режим Выбора (Selection) работает в обе стороны. Если я кликну на  строка кода, то будет выбраны (голубой рамкой) элементы UI, представляющие этот код:

И наоборот, если вы выберите элемент UI, то будет подсвечен код, представляющий этот элемент UI. Другими словами, дело не только в том, что ВЫ выбираете, а что выбирает XCode. Если вы выберете любой из двух вариантов, то будет выбран другой, поэтому я хочу, чтобы вы это поняли. 
И тогда как программисту, почему бы вам не повозиться с цвет или с некоторыми отступами или с чем-то еще в этом роде без необходимости постоянно печатать и редактировать исходный код. Это зависит от того, что вам нравится. Лично я люблю редактировать исходный код. Это как бы вытекает из моего мироощущения, поэтому мне нравится все делать в коде. Но некоторым людям нравится кликать на кнопки. И поэтому я просто не хотел принижать возможности Инспектора и режима Выбора (Selection). Это может быть интересным и значимым.
Итак, это было единственное, к чему из прошлой Лекции я хотел вернуться .

Настройки проекта  Project Settings 

Еще одна вещь, которую я хотел бы упомянуть.  Когда мы вышли на Навигатор в раздел Файлы, я сказал: “Посмотрите, есть три файла: MemorizeApp, ContentView и Assets. Но на самом деле есть еще одна вещь,  вы можете кликнуть на самой верхней строке иерархии, это что-то вроде файла, и увидеть настройки Project Settings вашего проекта. 

В самой верхней части настройки находится раздел Supported Destination, в котором указываются устройства, для которых создается приложение: iPhone, iPad и Mac (Designed для iPad):

Далее следует используемая операционная система iOS 16.4:

На следующей закладке представлены Signing & Capabilities, вот где вы имеете дело с подключением к своему Apple ID.

Xcode — это очень мощный механизм сборки (build engine) для создания  множества вещей, у которых есть зависимости (dependencies): фреймворки и приложения, зависящий друг от друга,  Поэтому у Xcode много всяких функциональностей для управления такого рода сложностями. 

Что такое some View?

Еще одна вещь, которую я хотел упомянуть. Она состоит в том, что собой представляет : View?

Вам всем сейчас должно быть очень комфортно с этим кодом, который означает, что этот ContentView «ведет себя как» View. Это, опять же, не имеет ничего общего с объектно-ориентированным программированием. 0%. Это не его суперкласс superclass или что-то подобное. В функциональном программировании нет суперклассов superclass. Они связаны исключительно с объектно-ориентированным программированием. 

:View означает, что структура struct ContentView «ведет себя как» View, и я подробно расскажу о том, что это такое “вести себя как ..” в самом начале будущей недели, но сейчас я просто хочу это подчеркнуть.

Еще одна вещь, которую я хотел бы подчеркнуть, это немного о том, как работает «some View«,

У меня есть маленькое демо. Я на секунду уберу весь код из var body в ContentView и размещу там просто текст Text («hello» ):

Итак var body — это some View и что это за ТИП? Это ТИП Text, потому что внутри фигурных скобок расположен только Text. Заметьте, если я изменю some View на Text, то код компилируется, следовательно, это верно:

some View — это способ заставить компилятор помочь вам, и он все еще продолжает делает это.
Но если я размещу в var body что-то еще, кроме Text («hello»), например, VStack, в котором находится Text («hello» ) и еще одна фраза Text («there» )

Теперь компилятор будет “жаловаться”, потому что этот код вычисляет значение, ТИП которого НЕ Text, так что теперь у вас есть несоответствие. На самом деле, если вы посмотрите на ошибку, то там очень подробно написано: “Cannot convert return expression of type  ‘VStack<TupleView<(Text, Text)>>‘ to return type Text” (Не могу преобразовать возвращаемое выражение ТИПа ‘VStack<TupleView<(Text, Text)>>‘ в возвращаемый ТИП Text.).

Обратите внимание, насколько сложен ТИП того, что возвращается вычисляемой переменной var bodyVStack<TupleView<(Text, Text)>>.
Я говорил вам, что маленький VStack мешочек LEGO называется TupleView, и вы видите это прямо сейчас.

В частности, это TupleView, у которого внутри два Text, и все это как бы закодировано в ТИП VStack выражение. Вот зачем нам нужен some View, потому что было бы слишком назойливо знать все эти подробности. Хорошая новость состоит в том, что вам вообще не нужно знать ничего из этого и на этом курсе вы никогда не будете печатать TupleView. Все это за вас за кулисами будет делать механизм @ViewBuilder.
Вам ничего не нужно знать ни о ТИПе, ни о доступе. Это всё полностью будет сделано вместо вас, и именно поэтому у нас есть это действительно замечательное some View, которая мы поставим на место Text

И посмотрите!
Это компилируется, всё работает нормально.
Понимаете?
Это объясняет наличие VStack<TupleView<(Text, Text)>>. Я просто хочу убедиться, что теперь мы понимаем немного больше. 

Позвольте мне вернуться туда, где мы были. Я возвращаю наш UI.

Управление версиями Source Control


И еще одна важная вещь, которую я забыл: отправьте это на GitHub

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

Мы внесли массу изменений в код. Поэтому я просто пойду в Source Control (систему контроля версий) и выберу Commit... , чтобы зафиксировать Commit эти изменения в моем репозитории.

Открывается замечательное окно, которое показывает все внесенное вами изменения.

Вы видите здесь массу изменений. Все файлы находятся слева, потому что это мой первоначальный Commit. Я ничего не использовал Commit, так что изменилось буквально всё. Мой проект совершенно новый. Я всегда хочу оставить комментарии, правда вам не позволяется выполнять Commit, не оставляя комментариев. 
Что же за комментарии это будут?
Во-первых, это мой первый CommitInitialCommit.
Но я также собираюсь вставить комментарий, который напомнит мне, для чего собственно мы используем Commit. Если бы я не забыл сделать это прошлый раз, то я бы оставил комментарий «ready to start doing Memorize» («готов приступить к реализации игры Memorize»), но мы уже прошли эту стадию и уже начали создавать игру Memorize, мы уже создали несколько карт, так что теперь я добавлю комментарий CardView, isFaceUp для CardView.

Так что я просто помещаю сюда комментарии, чтобы иметь возможность быстро вспомнить, что это был за коммит. Теперь я могу кликнуть на кнопке Commit 11 files
Обратите внимание, что я также могу отметить опцию Push to remote, сделав все за один шаг, если захочу. Но я буду делать это в два этапа. Сейчас я делаю свой Commit
Теперь, если я хочу выполнить submit своего Задания, потому что именно это вы и будете делать на этом курсе, я просто кликну Source Control / Push

Поскольку я создал этот проект внутри репозитория, который я клонировал, он видит это  в origin / main

Убедитесь, что вы всегда выбираете  origin / main  и никогда не используете origin / feedback

Я покажу вам через секунду, что собой представляет ветка origin / feedback. Не беспокойтесь о тегах (tags), которые мы не собираемся применять. И просто кликните Push. Происходит подключение к GitHub и все изменения оказываются в вашем репозитории. И если я сейчас зайду на GitHub, то я увижу там все файлы, которые я только что отправил с помощью кнопки Push. В следующий раз, когда мы нажмем Push ( надеюсь, что не забуду снова нажать Push где-нибудь в середине этой демонстрации), мы сможем увидеть в Xcode различия между актуальным кодом и тем, который был послан на GitHub раньше.
Это действительно круто, особенно если вы не забудете сделать то, что я не сделал.

Теперь об этой ветке origin / feedback. Если вы посмотрите на самый верх экрана слева, то увидите, что там написано Feedback, если вы кликните на этом, появится  Pull Request:

Вы можете кликнуть на нем,  в результате он вытащит все из ветки origin / feedback на GitHub, и вы действительно сможете посмотреть на эту информацию.

Это оценки (Grading Feedback) ваших Домашних Заданий, поэтому, когда ваши ассистенты оценивают ваши Задания, то эта информация начнет появляться здесь. Так что в любое время, когда хотите, вы можете это посмотреть. Вы видите #1 Feedback, он становится закладкой, которую вы видите вверху:

Вы можете переключатся между своим кодом (ContentView) и оценкой (#1 Feedback), которую вы получили. Это действительно круто.
Итак, это интеграция с GitHub. Немногое можно сказать об этом. Это мы делаем впервые. Похоже, это работает очень хорошо. У меня был только один человек, который не смог клонировать репозиторий. 
Еще одна вещь, как только вы начнете вносить изменения и редактировать ваш код, то увидите букву M (modified) для того файла, который изменяется
.

Если вы добавляете (add) файл, то получите для него букву скажите A, чтобы вы могли видеть, что делаете.

Синтаксис хвостового замыкания ( trailing closure syntax)

Первое, что я хотел обсудить из того, что было представлено в прошлый раз, и я напишу это снова здесь, это trailing closure syntax (синтаксис хвостого замыкания). Само хвостовое замыкание ( trailing closure) на рисунке ниже выделено:

У нас был ZStack, и у него есть аргумент content, который берет мешок LEGO и организует его. И внутри даже есть какое-то условное выражение. В зависимости от того isFaceUp равен true или false , формируется различный список Views.

Мы помним, что у ZStack могут быть и другие аргументы, например, выравнивание — alignment: .top

Смотрите, теперь наше Привидение 👻выровнено по верху. Прямоугольники RoundedRectanglet и Привидение 👻все еще находятся один поверх другого, но Привидение 👻- наверху.

Мы используем всего лишь два аргумента для создания ZStack:

ZStack — это просто структура struct, она ведет себя как View и оно имеет два аргумента. Если последним аргументом при создании чего-либо или функции является сама функция, подобная этим фигурным скобкам…
Помните? Этот мешок с LEGO на самом деле является функцией, возвращающей View, один из тех TupleViews или что-то в этом роде — коллекция View. Если последний аргумент — это функция, тогда вы можете сделай вот что: избавится от метки, если она у него есть, a в этом случае она обычно у него есть, перенести сюда закрывающую круглую скобку  и дать функции, по существу, “повиснуть” на конце:

Видите?
Функция просто “висит” на конце. Это называется синтаксисом хвостового замыкания (trailing closure syntax), потому что эта функция, оформленная в виде фигурных скобок, называется замыканием (closure).

Мы поговорим обо всем этом: о встроенных функциях, которые мы называем их замыканиями, о том, что они расположены в конце и могут там зависать. Вот почему код выглядит именно так.
Но идем дальше и я сказал, что нам не нужно выравнивание по верху alignment: .top, мы хотим иметь выравнивание по центру alignment: .center, которое оказалось значением по умолчанию, как false в случае с isFaceUp, поэтому этот параметр для ZStack мне вообще не нужен.

Компиляция проходит, отлично.
Я иду еще дальше и удаляю эти круглые скобки.

Этот шаг я могу сделать только в том случае, если у меня есть синтаксис хвостового замыкания (trailing closure syntax). Вы не можете удалить пустые круглые скобки при вызове функции или создании структуры, если у вас нет синтаксиса хвостового замыкания (trailing closure syntax).
Просто хочу прояснить это сейчас. 

fill() для геометрических фигур Shape

Кстати, я хочу поговорить здесь также о fill().
Вы видите этот RoundedRectangle (cornerRadius: 12) внизу?

Кажется, что просто сидеть там, а на самом деле он является оборотной стороной карты и в действительности использует .fill().

Точно так же, как .strokeBorder() обводит края этого прямоугольника, .fill() закрашивает его. И это значение является значением по умолчанию. Если вы не указываете геометрической фигуре ни закрашивать .fill(), ни обводить .strokeBorder() , то геометрическая фигура будет просто закрашена. Это просто его значение по умолчанию. но сам по себе модификатор .fill() очень классный, потому что позволяет закрасить определенным цветом, например, белым fill(.white) можно закрасить лицевую сторону карты:

Значение по умолчанию для переменной var

Сейчас я хочу вернуться туда, где мы были, когда остановились в прошлый раз, только что добавив сюда переменную var isFaceUp и присвоив ей значение по умолчанию false:

Я просто хочу подчеркнуть, что если бы я удалил это значение по умолчанию, то получил бы “жалобы” компилятора, потому, что я сказал вам, что если у вас есть переменная var в структуре struct — любой структуре struct, а не только в структуре struct, которая ведет себя как View.

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

Вот почему на верхнюю строку CardView(isFaceUp: true ) компилятор не “жалуется” a на другие строки CardView( ) жалуется. Потому что вы пытаетесь создать структуру CardView, которая имеет переменную var isFaceUp, значение которой никогда не было установлено.
Все понимают, что здесь происходит?

Если я скопирую isFaceUp: true и вставлю это в другие CardView, то компилятор прекратит “жаловаться”:

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

Это позволяет нам удалить назначение аргументу isFaceUp значения true при инициализации карты, так как это является значением по умолчанию:

Мы видим, что карта CardView() по-прежнему лежит лицевой стороной вверх.

Давайте поговорим еще немного о сумке LEGO, которую мы передаем в ZStack для создания наших карт.

Локальные переменные в @ViewBuilder

Мы знаем, что в @ViewBuilder сумке LEGO мы можем создавать списки Views, использовать условные выражения if else, но есть еще одна вещь, которую мы можем использовать, это локальные переменные. В этом плане я мог бы, например, использовать переменную var base, имеющую ТИП RoundedRectangle и равную RoundedRectangle (cornerRadius: 12):

В нашем @ViewBuilder мы используем RoundedRectangle (cornerRadius: 12) три раза, что очень плохо, потому что это повторяющийся код.
Поэтому я создал переменную var base, которую собираюсь разместить вместо RoundedRectangle (cornerRadius: 12), который используется трижды в коде:

Вы видите, что я сделал: создал локальную переменную var base.
Вы можете сказать: «О нет, но это же View
Хорошо, но View — это просто структура struct. Переменная могла быть Int и вы бы не так испугались этого?
Int — это просто структура struct. RoundedRectangle является View и геометрической фигурой Shape, но в остальном это всего лишь структура struct, так что нет причин, по которым я не могу создать его здесь.
Я мог бы также создать переменную var i: Int = 1, это совершенно законно, то есть я могу создавать любые локальные переменные. Однако я не могу зайти так далеко, чтобы написать var i: Int = 1 и x = x + 1:

Теперь я действительно зашел слишком далеко, такой код не может находиться в @ViewBuilder:

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

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

  • Константа let и переменная var
  • Вывод ТИПа из контекста (Type Inference)
  • Модификатор ViewModifier. onTapGesture
  • Печать print на консоли
  • @State
  • Массив Array
  • ForEach— цикл for для SwiftUI 
  • Кнопка Button
  • Изображение Image(systemName:)
  • Рефакторинг кода в ContentView
  • Неявный return
  • Вместо копирования и вставки — функция func
  • Внешние (external) имена и внутренние (internal) имена для параметров
  • View модификатор .disabled для кнопки Button
  • Сетка LazyVGrid
  • Вместо if else используем непрозрачность opacity
  • Соотношение сторон aspectRatio

Целиком конспект Лекции 2 можно прочитать на Google Doc здесь.
Код находится на GitHub.

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