Merge branch 'main' into fix/sprint-11

This commit is contained in:
Harley
2025-10-21 11:20:54 +03:00
committed by GitHub
243 changed files with 6017 additions and 2798 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 337 KiB

@@ -84,21 +84,28 @@ Xcode — интегрированная среда для iOS-разработ
Дважды кликните на `Pong.xcodeproj`. Он скачан из интернета — Xcode выведет предупреждение о безопасности:
![image](/images/part-6/xcode-warning-file-downloaded-from-internet.png)
![image](.././images/part-6/xcode-warning-file-downloaded-from-internet.png)
Выберите `Trust and Open` (или `Доверять и открыть`). Откроется окно; в левой части нажмите на строку `Pong` с синей иконкой в начале:
![image](/images/part-6/pong-project-first-launch.png)
![image](.././images/part-6/pong-project-first-launch.png)
Перед вами Xcode во всей красе! Кнопок и панелей много, но не пугайтесь: самые важные элементы мы разберём на курсе, а оставшаяся часть, возможно, никогда вам не понадобится.
![image](/images/part-6/illustration-student-and-huge-panel.png)
![image](.././images/part-6/illustration-student-and-huge-panel.png)
## Как запустить приложение Pong
Чтобы почувствовать себя молодым Стивом Джобсом и запустить приложение, выберите устройство в Xcode. В верхней части интерфейса найдите меню рядом с кнопкой запуска ▶️. В нём можно выбрать симулятор — выберите **iPhone 15**.
Чтобы почувствовать себя молодым Стивом Джобсом и запустить приложение, нужно выбрать устройство для запуска в Xcode.
В верхней части интерфейса найдите меню рядом с кнопкой запуска ▶️.
![image](/images/part-6/select-iphone-for-pong-project-simulator-launch.png)
> Если вы только что скачали Xcode версии 15 и выше, то Xcode предложит скачать необходимый симулятор: нажмите на кнопку Get, как показано на скриншоте. После того как загрузка завершится, продолжайте урок.
Теперь можно выбрать симулятор — выберите **iPhone 15** или **iPhone 16**.
> В 2025 году актуальной версией, доступной для выбора, является iPhone 16. На скриншотах ниже вы увидите iPhone 15 — работа с ним аналогична.
![image](.././images/part-6/select-iphone-for-pong-project-simulator-launch.png)
> Симулятор — виртуальное устройство, которое имитирует работу iPhone или iPad на компьютере. Он идеально подходит для тестирования приложений в разных условиях: от перемещения пользователя в пространстве до нестабильности сетевого подключения.
@@ -67,7 +67,7 @@
## Открываем файлы
Чтобы выбрать файл и открыть его, щёлкните по названию файла курсором. Например, выберите файл `AppDelegate.swift`:
Чтобы выбрать файл и открыть его, щёлкните по названию файла курсором. Например, выберите файл `SceneDelegate.swift`:
![image](/images/part-8/xcode-file-selected.png)
@@ -33,7 +33,7 @@
Одна переменная типа `Int` может хранить одно целое число, которое не больше 2147483647 и не меньше -2147483648. Запоминать точный диапазон допустимых значений не нужно: Swift подскажет, если присваиваемое значение не соответствует указанному типу.
Для хранения переменной типа `Int` система резервирует память (даже если в переменной будет храниться число 0). Объём резерва зависит от архитектуры процессора: для iPhone 5, например, это будет 32 бита, для iPhone 7 — уже 64.
Для хранения переменной типа `Int` система резервирует память (даже если в переменной будет храниться число 0). Объём резерва зависит от архитектуры процессора: для старых моделей iPhone (до 5 включительно) и для Apple Watch (из-за ограничений памяти) это будет 32 бита, для современных моделей iPhone — уже 64.
> Сохраняем значение целочисленной цены товара в переменную `price`:
@@ -202,7 +202,7 @@ func testFetchStations() {
// 1. Создаём экземпляр сгенерированного клиента
let client = Client(
// Используем URL сервера, также сгенерированный из openapi.yaml (если он там определён)
serverURL: try Servers.server1(),
serverURL: try Servers.Server1.url(),
// Указываем, какой транспорт использовать для отправки запросов
transport: URLSessionTransport()
)
@@ -89,6 +89,6 @@
Если после выполнения обязательных требований вы хотите получить больше практического опыта со SwiftUI, вы можете:
1. Добавить нестандартные анимации переходов между экранами. Например, сделать так, чтобы экраны появлялись не снизу, а сбоку или сверху.
1. Добавить нестандартные анимации переходов между экранами. Например, сделать так, чтобы экраны появлялись не сбоку, а снизу или сверху.
Удачи! Всё получится!
@@ -1,3 +1,5 @@
# Онбординг
Добро пожаловать в 20-й спринт!
Предыдущий спринт был очень насыщенным, вы узнали много нового:
@@ -7,7 +9,7 @@
- поработали с таблицами;
- а так же узнали и попробовали на практике, как организуется навигация.
Ну а этот спринт открывает перед вами мир анимаций в SwiftUI. Анимации играют важную роль в приложениях — с ними взаимодействие пользователя с вашим приложением становится приятным.
А этот спринт открывает перед вами мир анимаций в SwiftUI. Анимации играют важную роль в приложениях — с ними взаимодействие пользователя с вашим приложением становится приятным.
## О важности анимаций
@@ -15,13 +17,14 @@
Посмотрите на приложения, которыми вы часто пользуетесь. Обратите внимание на анимации, реализованные в них. Нравится ли вам, то что вы видите? Хотели бы вы что-то поменять или улучшить?
Например, интересным образцом применения анимаций, и для увеличения привлекательности, и для улучшения пользовательского опыта, является стандартное для iOS приложение погоды (Weather). В нём в верхней части вы можете видеть малополезную, но очень красивую анимацию погодных явлений: дождь, движение туч, и прочее. Но там есть и примеры полезной анимации: например, при работе с графиком прогноза на 10 дней.
Например, интересным образцом применения анимаций, и для увеличения привлекательности, и для улучшения пользовательского опыта, является стандартное для iOS приложение погоды (Weather). В нём в верхней части вы можете видеть малополезную, но очень красивую анимацию погодных явлений: дождь, движение туч, и прочее. Но там есть и примеры полезной анимации: например, при работе с графиком прогноза на 10 дней.
## Программа спринта
В сравнении с UIKit, в SwiftUI возможности для создания анимаций расширились и упростились, а их представления стали привлекательнее и производительнее.
В сравнении с UIKit, в SwiftUI возможности для создания анимаций расширились и упростились, а их представления стали привлекательнее и производительнее.
В двадцатом спринте вы:
В двадцатом спринте вы:
- узнаете, как можно использовать формы вместе с другими вью;
- научитесь добавлять жесты касания и перетаскивания;
- рассмотрите важнейшие элементы для создания анимаций;
@@ -29,6 +32,7 @@
- увидите, какие события могут начинать и заканчивать анимации.
А ещё вы будете много практиковаться:
- сначала на простых, а затем на более сложных примерах посмотрите, как создаются анимации;
- узнаете, что такое множественные анимации;
- поработаете с анимациями переходов.
@@ -41,13 +45,11 @@
## Завершение спринта
Ну и в заключении, чтобы закрепить полученные знания, вас будет ждать задание на доработку приложения «Расписание Путешествий». Как и в предыдущих спринтах, вы сдадите выполненное задания на ревью и получите обратную связь от ревьюверов.
> Если хотите освежить знания по SwiftUI и Combine, вернитесь к [спринту 16](https://practicum.yandex.ru/) или [спринту 19](https://practicum.yandex.ru/).
Чтобы закрепить полученные знания, вас будет ждать задание на доработку приложения «Расписание Путешествий». Как и в предыдущих спринтах, вы сдадите выполненное задания на ревью и получите обратную связь от ревьюверов.
# Подведём итоги
В этом уроке мы познакомили вас с темами, которые предстоит изучить. Впереди вас ждут увлекательные уроки!
КНОПКА
КНОПКА
Вперёд!
@@ -1,11 +1,11 @@
# Введение
# Формы и жесты
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
Пользователи ценят в мобильных приложениях не только функциональность, а ещё красивый внешний вид и удобство. Причём мало кто говорит, что ему нравятся плавный скролл, приятное листание картинок в галерее и бодрое появление всплывающего меню с лёгким эффектом подпрыгивания.
В хорошем отзыве в сторе обычно пишут, что у вас «отличное современное приложение», не более. А вот если что-то пойдёт не так, то тут появляются достаточно конкретные комментарии о том, что «тут тормозит, там дёргается, вот это некрасиво, а это неудобно».
![img](https://github.com/Yandex-Practicum/mobile-iOS/blob/main/lessons-extended-program/20.sprint/01.%20Анимации%20в%20SwiftUI/pictures_in_color/1%20урок_%20подвисает.png)
Анимация нужна, чтобы у пользователя сложилось приятное впечатление о приложении. И к счастью, SwiftUI даёт много встроенных возможностей для реализации этого!
В этом уроке мы познакомимся с анимацией при помощи жестов tap и drag — касания и перетаскивания. Мы ежедневно сталкиваемся с касанием, когда нажимаем на кнопки в приложениях, а с перетаскиванием — когда листаем галерею картинок. Однако помимо таких встроенных возможностей, мы можем задействовать эти жесты на произвольных view.
@@ -36,7 +36,7 @@
Создайте новый проект для экспериментов и внутри body напишите вот так:
> Только не копируйте все строки, а последовательно добавляйте по одной.
> Только не копируйте все строки сразу, а последовательно добавляйте по одной.
```swift
Circle()
@@ -49,6 +49,7 @@ RoundedRectangle(cornerRadius: 25.0)
> Последнюю форму мы не включили, чтобы остановиться на ней подробнее ближе к середине урока.
![Так выглядят формы, если добавлять их по очереди](/lessons-extended-program/20.sprint/assets/img/l1_scr01.png)
*Так выглядят формы, если добавлять их по очереди*
Здесь сразу можно проследить закономерности.
@@ -68,10 +69,12 @@ RoundedRectangle(cornerRadius: 25.0)
ПРАКТИКУМ: Потому что их наполнение по умолчанию — это Color.primary, значение, которое напрямую зависит от светлой или тёмной темы.
![На тёмной теме фон чёрный, формы белые](/lessons-extended-program/20.sprint/assets/img/l1_scr02.png)
*На тёмной теме фон чёрный, формы белые*
> Чтобы фигура стала, например, зелёной, добавьте к ней модификатор `.fill(Color.green)`.
![Прямоугольник перекрашен в зелёный цвет](/lessons-extended-program/20.sprint/assets/img/l1_scr03.png)
*Прямоугольник перекрашен в зелёный цвет*
КНОПКА-ДИАЛОГ:
Студент: А разве в мобильных приложениях часто нужны геометрические фигуры?
@@ -92,6 +95,7 @@ RoundedRectangle(cornerRadius: 25.0)
- Ещё сделаем цветной фон в чате и отдельный фон вокруг реплик.
![Так это будет в итоге](/lessons-extended-program/20.sprint/assets/img/l1_scr21.png)
*Так это будет в итоге*
Для начала набросаем основную структуру.
@@ -146,6 +150,7 @@ VStack {
```
![Начинаем вёрстку чата](/lessons-extended-program/20.sprint/assets/img/l1_scr04.png)
*Начинаем вёрстку чата*
Это уже похоже на то, что нам нужно.
@@ -161,10 +166,12 @@ VStack {
> В большинстве случаев порядок модификаторов не так уж важен, но иногда он критичен. Если мы поставим `background` до `padding`, у нас образуются вот такие некрасивые поля:
![Такие поля нам не нужны.](/lessons-extended-program/20.sprint/assets/img/l1_scr06.png)
*Такие поля нам не нужны*
> А если `background` стоит после `padding`, то всё выглядит куда лучше:
![Вот так годится!](/lessons-extended-program/20.sprint/assets/img/l1_scr05.png)
*Вот так годится!*
Теперь применим сюда встроенные формы.
@@ -174,7 +181,7 @@ VStack {
.clipShape(Circle())
```
> Порядок имеет значение и здесь: если этот модификатор будет стоять `background`, ничего не получится, картинка останется квадратной.
> Порядок имеет значение и здесь: если этот модификатор будет стоять до `background`, ничего не получится, картинка останется квадратной.
А ко всем элементам Text надо добавить три строчки:
@@ -185,6 +192,7 @@ VStack {
```
![Должно получиться вот так](/lessons-extended-program/20.sprint/assets/img/l1_scr07.png)
*Должно получиться вот так*
КНОПКА-ДИАЛОГ
Студент: Уже неплохо!
@@ -207,6 +215,7 @@ Text("Привет!")
Готово!
![Всё ближе к нужному результату](/lessons-extended-program/20.sprint/assets/img/l1_scr08.png)
*Всё ближе к нужному результату*
Теперь у нас левый верхний угол указывает на автора сообщения — это делает структуру чата более понятной визуально. Такой приём часто используется в приложениях, например, в Viber.
@@ -222,6 +231,7 @@ Text("И тебе привет!")
```
![Почти то, что нужно!](/lessons-extended-program/20.sprint/assets/img/l1_scr09.png)
*Почти то, что нужно!*
Теперь только нижняя реплика выбивается из стиля, но это можно исправить: замените `Capsule` на `RoundedRectangle` с подходящим параметром радиуса.
@@ -256,6 +266,7 @@ Text("У нас сегодня дождь.")
Взгляните, несмотря на то, что мы указали, что форма view — это прямоугольник с закруглёнными углами, углы у границы совсем не закруглены!
![Прямоугольная граница](/lessons-extended-program/20.sprint/assets/img/l1_scr10.png)
*Прямоугольная граница*
Может, стоит поменять местами `border` и `clipShape`?
@@ -268,6 +279,7 @@ Text("У нас сегодня дождь.")
```
![Рваная граница](/lessons-extended-program/20.sprint/assets/img/l1_scr11.png)
*Рваная граница*
КНОПКА-ДИАЛОГ
Студент: Кажется, так выглядит хуже?
@@ -289,6 +301,7 @@ Text("У нас сегодня дождь.")
```
![Отличная граница](/lessons-extended-program/20.sprint/assets/img/l1_scr12.png)
*Отличная граница*
КНОПКА
Ура, получилось!
@@ -312,6 +325,7 @@ Circle()
```
![Круг с обводкой](/lessons-extended-program/20.sprint/assets/img/l1_scr13.png)
*Круг с обводкой*
Смотрите, какой эффект: мы знаем, что форма сама по себе автоматически вписывается в размеры родительского view, но это не распространяется на обводку. Это стоит иметь в виду, проектируя такие экраны.
@@ -322,6 +336,7 @@ Circle()
```
![Круг с обводкой и отступом](/lessons-extended-program/20.sprint/assets/img/l1_scr14.png)
*Круг с обводкой и отступом*
Как видите, отступ тоже считается без учёта обводки. Об этом вам придётся заботиться вручную.
@@ -338,6 +353,7 @@ Circle()
```
![Что-то пошло не так](/lessons-extended-program/20.sprint/assets/img/l1_scr15.png)
*Что-то пошло не так*
КНОПКА-ДИАЛОГ
Студент: И наша обводка только оранжевая. Не получилось?
@@ -354,6 +370,7 @@ Circle()
```
![Тройная обводка](/lessons-extended-program/20.sprint/assets/img/l1_scr16.png)
* Тройная обводка*
Оказывается, у нас действительно три обводки! Просто верхние разместились по центру нижней.
@@ -393,9 +410,10 @@ struct ContentView: View {
Попробуйте поэкспериментировать и определить, в чём проблема с этой кнопкой?
ОТВЕТ
Проблема в том, что кнопка круглая, а область нажатия у неё квадратная. Это легко увидеть, если нажать в любом из уголков квадрата, в который вписан этот круг — внизу в консоли мы увидим сообщение об удачном нажатии. Мы уже за пределами кнопки, но она доступна для нажатия, но так быть не должно.
Проблема в том, что кнопка круглая, а область нажатия у неё квадратная. Это легко увидеть, если нажать в любом из уголков квадрата, в который вписан этот круг — внизу в консоли мы увидим сообщение об удачном нажатии. Мы уже за пределами кнопки, но она доступна для нажатия, и так быть не должно.
![Угол доступен для нажатия](/lessons-extended-program/20.sprint/assets/img/l1_gif01.gif)
*Угол доступен для нажатия*
Чтобы избежать такого поведения, необходимо добавить к Button ещё один модификатор:
@@ -415,6 +433,7 @@ struct ContentView: View {
Ещё одно полезное применение форм — это их сочетания. Возможно, вы видели приложение «Дыхание» на Apple Watch — там нарисовано что-то вроде цветочка. Вот так это выглядит на официальных скриншотах из AppStore:
![Скриншоты приложения «Дыхание»](/lessons-extended-program/20.sprint/assets/img/l1_scr20.png)
*Скриншоты приложения «Дыхание»*
Давайте разберёмся, как такое нарисовать.
@@ -447,6 +466,7 @@ ZStack {
```
![Пока всё один круг...](/lessons-extended-program/20.sprint/assets/img/l1_scr17.png)
*Пока всё один круг...*
Конечно, изначально они просто лягут один над другим, а нужно заставить их сместиться от центра в сторону. Сделаем это при помощи модификатора `offset`.
@@ -463,6 +483,7 @@ ZStack {
```
![Два круга разошлись в стороны](/lessons-extended-program/20.sprint/assets/img/l1_scr18.png)
*Два круга разошлись в стороны*
Обратите внимание: здесь не нужно высчитывать центры для кругов или контролировать их как-то иначе. Мы просто пользуемся тем, что форма стремится занять всё доступное место, и говорим ей: займи лишь половину размера с той стороны, куда мы указали.
@@ -478,9 +499,10 @@ Circle()
.offset(y: -size/2)
.rotationEffect(Angle(degrees: 60))
```
У одной пары мы укажем угол поворота 60 градусов, а у другой - 120.
У одной пары мы укажем угол поворота 60 градусов, а у другой - 120, добавьте её самостоятельно.
![А вот и шесть кругов!](/lessons-extended-program/20.sprint/assets/img/l1_scr19.png)
*А вот и шесть кругов!*
Ура! Всего лишь при помощи шести кругов мы нарисовали цветок!
@@ -597,7 +619,7 @@ ZStack {
Формы: https://developer.apple.com/documentation/swiftui/shapes
Жесты: https://developer.apple.com/documentation/swiftui/gestures
# Подведём итоги
## Подведём итоги
В этом уроке вы узнали, что в SwiftUI существуют готовые геометрические фигуры, которые также называются встроенными формами. Их можно размещать на экране сами по себе, а можно применять к другим view, придавая им форму, накладывая маску или задавая форму контента.
@@ -607,23 +629,40 @@ ZStack {
А в следующих уроках мы поговорим об анимации более детально: какие бывают виды, как и для чего они применяются, и какими средствами располагает SwiftUI, чтобы мы смогли анимировать всё, что захотим. Спойлер: этих средств довольно много!
# Повторим изученное
## Повторим изученное
**КВИЗ - НАЧАЛО**
1. Что из перечисленного относится к встроенным формам?
[х] Круг (Circle)
Пояснение: Верно! Круг — одна из встроенных форм.
[х] Эллипс (Ellipse)
Пояснение: Да! Эллипс относится к встроенным формам.
1. Какие из перечисленных ниже элементов являются конкретными, готовыми к использованию встроенными формами в SwiftUI, а не протоколами, обобщениями или средствами для создания произвольных фигур?
[ ] Треугольник (Triangle)
Пояснение: Увы, но готового треугольника не существует. Если понадобится — придётся рисовать самим.
[х] `Circle`
Пояснение: `Circle` — это одна из стандартных встроенных форм.
[х] `Ellipse`
Пояснение: Да! `Ellipse` также является готовой к использованию встроенной формой.
[ ] `Triangle`
Пояснение: Увы, но готового треугольника не существует. Если понадобится — придётся рисовать самим с помощью `Path`.
[ ] Квадрат (Square)
Пояснение: Отдельной формы под названием «квадрат» не существует, потому что квадрат — это всего лишь прямоугольник (Rectangle), у которого равны длина и ширина.
2. Какой код, представленный ниже, придаст view с название OurView форму круга?
[х] `Rectangle`
Пояснение: `Rectangle` — это базовая встроенная форма. "Квадрат" является частным случаем `Rectangle`.
[ ] `Path`
Пояснение: `Path` — это мощный инструмент для создания произвольных фигур, но сам по себе не является *встроенной формой* в том же смысле, что `Circle` или `Rectangle`. Он используется для определения кастомных форм.
[х] `Capsule`
Пояснение: `Capsule` — это стандартная встроенная форма, часто используемая для кнопок.
[ ] `Shape` (протокол)
Пояснение: `Shape` — это протокол, которому должны соответствовать все формы, а не конкретная встроенная форма.
[х] `RoundedRectangle`
Пояснение: `RoundedRectangle` — это часто используемая встроенная форма для создания прямоугольников со скругленными углами.
1. Какой код, представленный ниже, придаст view с название OurView форму круга?
[ ] Вариант 1
```swift
@@ -637,7 +676,7 @@ OurView()
OurView()
.clipShape(Circle())
```
Пояснение: Верно! Самый простой способ.
Пояснение: Самый простой способ.
[x] Вариант 3
OurView()
@@ -657,21 +696,24 @@ OurView()
```
Пояснение: Нет, модификатора circle() тоже не существует.
3. Как создать разные закругления на углах прямоугольника?
1. Вам нужно создать UI-элемент с двумя острыми и двумя скруглёнными углами. Какой инструмент наиболее точно и эффективно решит эту задачу?
[ ] Нет такого способа.
Пояснение: Такой способ есть. Возможно, вам стоит перечитать главу.
[ ] Использовать `Path` (`UIBezierPath`) для ручной отрисовки контура с необходимыми скруглениями и прямыми углами.
Пояснение: Технически возможно, но избыточно сложно для задачи, имеющей стандартное решение. `Path` предназначен для более комплексной геометрии, где стандартные формы не подходят.
[ ] Наложить друг на друга прямоугольники с разным радиусом на углах.
Пояснение: Увы, не сработает. Одни закругления просто закроют другие.
[x] Применить `UnevenRoundedRectangle`.
Пояснение: `UnevenRoundedRectangle` в сочетании с `RectangleCornerRadii` позволяет задать индивидуальный радиус для каждого из четырех углов, что идеально подходит для данного сценария.
[x] Использовать UnevenRoundedRectangle.
Пояснение: Верно! Его специально для этого и придумали.
[ ] Комбинировать два `RoundedRectangle` с разными `cornerRadius` через `.overlay()` и/или `.mask()`.
Пояснение: Такой подход будет очень сложным в реализации и трудно реализуемым и непредсказуемым. Результат скорее всего будет некорректным.
[ ] Наложить несколько разных масок с разными закруглениями.
Пояснение: Маски просто скроют друг друга, и в итоге останется то закругление, которое окажется наименьшим.
[ ] Использовать модификатор `.cornerRadius` и затем "выпрямить" два угла с помощью отрицательных отступов при наложении непрозрачных прямоугольников поверх углов.
Пояснение: Модификатор `.cornerRadius()` применит скругление ко всем углам. Попытки "исправить" это другими способами приведут к некачественному визуальному результату и усложнят код.
4. Можно ли заставить view реагировать на два жеста одновременно?
[ ] Создать четыре отдельных `View` для каждого угла, два из которых будут квадратами, а два — секторами круга, и затем скомпоновать их в `ZStack` и/или `HStack/VStack`.
Пояснение: Это чрезмерно усложненный, неэффективный и трудно поддерживаемый подход для задачи управления радиусами углов одного элемента. Стандартные формы предлагают гораздо более элегантное решение.
4. Можно ли заставить view реагировать на два жеста одновременно?
[ ] Нет. Надо выбрать какой-то один жест и добавить соответствующий модификатор.
Пояснение: Это не так. Мы можем добавить два жеста сразу.
@@ -683,7 +725,7 @@ OurView()
Пояснение: Платформа не ограничивает нас в выборе жестов.
[x] Да! Мы можем добавить несколько модификаторов для разных жестов.
Пояснение: Верно!
Пояснение: Добавление нескольких модификаторов для разных жестов вполне возможно.
5. Чтобы view реагировало на жест «двойное касание», надо:
@@ -694,7 +736,7 @@ OurView()
Пояснение: Нет. Так даже сделать не получится.
[x] Использовать модификатор gesture, а внутри него — SpatialTapGesture.
Пояснение: Верно! Это и есть правильный способ.
Пояснение: Это и есть правильный способ.
[ ] Использовать жест «растягивание» (MagnifyGesture) — он как раз делается двумя пальцами.
Пояснение: Увы. Про этот жест мы поговорим в следующем уроке, но здесь он нам никак не поможет.
@@ -1,3 +1,7 @@
# Основы анимации
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В прошлом уроке вы начали изучать анимации в SwiftUI:
- учились обрабатывать жесты «касание» и «перетаскивание»;
@@ -22,11 +26,14 @@
Пояснение: Keyframes тут не подойдут. Это трудоёмкий и неудобный способ, где вам придётся рассчитать вручную все кадры, и всё равно красивой плавной анимации в этом сценарии не достичь.
[x] Можно! Нам поможет CGAffineTransform, где мы можем указать угол поворота и смещение одновременно.
Пояснение: Совершенно верно!
Пояснение: `CGAffineTransform` в UIKit позволяет комбинировать различные аффинные преобразования, такие как поворот (rotation) и смещение (translation), в одну операцию. Это делает его подходящим инструментом для одновременного применения нескольких анимационных эффектов к `UIView`.
[ ] Нет. В UIKit можно применить две анимации, но только последовательно, одну за другой.
Пояснение: Анимации в UIKit можно применять и последовательно, и параллельно.
КНОПКА
Узнать ответ
2. Допустим, мы хотим запустить анимацию не сразу. Какой параметр отвечает за задержку старта анимации?
[ ] duration
@@ -39,22 +46,36 @@
Пояснение: Такого параметра не существует.
[ ] initialSpringVelocity
Пояснение: Это скорость «отпружинивания» анимации.
Пояснение: Это скорость «отпружинивания» анимации.
[ ] wait
Пояснение: Такого параметра не существует.
3. А если бы мы хотели плавно скрыть UIView, то могли бы использовать анимацию свойства isHidden?
КНОПКА
Узнать ответ
[ ] Да, анимировать можно что угодно!
Пояснение: К сожалению, есть свойства, для которых встроенных анимаций нет, и isHidden — одно из них.
3. Какое свойство `UIView` лучше всего анимировать для плавного скрытия элемента с экрана?
[x] Нет, для этого свойства нет стандартных средств.
Пояснение: Верно! Увы, простые инструменты есть не для всех свойств. Для isHidden придётся изобретать что-то своё.
[ ] `isHidden`
Пояснение: Свойство `isHidden` изменяется мгновенно и не поддерживает плавную анимацию. Элемент либо виден, либо нет.
[x] `alpha`
Пояснение: Анимация свойства `alpha` от 1.0 до 0.0 позволяет плавно скрыть элемент, делая его прозрачным.
[ ] `backgroundColor`
Пояснение: Изменение цвета фона не скроет элемент, а лишь изменит его внешний вид.
[ ] `frame.size.height` до 0
Пояснение: Хотя это и может скрыть элемент, анимация `alpha` обычно предпочтительнее для простого плавного исчезновения, так как изменение высоты может повлиять на макет других элементов и не всегда выглядит так же плавно.
КНОПКА
Узнать ответ
**КВИЗ - КОНЕЦ**
Теперь, когда вы вспомнили основы работы с анимацией в UIKit, самое время сообщить хорошую новость: хоть в базовых принципах анимация в обоих фреймворках не сильно отличается, однако в SwiftUI работать с ней стало удобнее — возможности расширились, улучшились внешний вид и производительность. Кроме того, не найти такого приложения, где в том или ином виде не применялась бы анимация: она есть в стандартных встроенных средствах — скроллинг, навигация; и в сложных кастомных — параллакс-эффект, фейерверк. Это может пригодиться вам и в учебных приложениях этого спринта!
Теперь, когда вы вспомнили основы работы с анимацией в UIKit, самое время сообщить хорошую новость: хоть в базовых принципах анимация в обоих фреймворках не сильно отличается, однако в SwiftUI работать с ней стало удобнее — возможности расширились, улучшились внешний вид и производительность.
Не найти такого приложения, где в том или ином виде не применялась бы анимация: она есть в стандартных встроенных средствах — скроллинг, навигация; и в сложных кастомных — параллакс-эффект, фейерверк. Это может пригодиться вам и в учебных приложениях этого спринта!
Интригует?
@@ -119,6 +140,7 @@ struct ContentView: View {
Теперь запустим код в превью и коснёмся глобуса либо надписи.
![Анимация scaleEffect](/lessons-extended-program/20.sprint/assets/img/l2_gif01.gif)
*Анимация scaleEffec*
Ещё давайте проверим, можно ли таким образом анимировать другие свойства или сразу несколько.
@@ -136,12 +158,14 @@ struct ContentView: View {
Попробуем запустить!
![Анимация с поворотом на 180 градусов](/lessons-extended-program/20.sprint/assets/img/l2_gif02.gif)
*Анимация с поворотом на 180 градусов*
КНОПКА-ДИАЛОГ
Студент: А если задать 360 градусов, то будет ли анимация? Ведь технически исходное положение view совпадает с начальным?
Практикум: Не будем спойлерить — лучше посмотрим.
![Анимация с поворотом на 360 градусов](/lessons-extended-program/20.sprint/assets/img/l2_gif03.gif)
*Анимация с поворотом на 360 градусов*
Так можно анимировать изменение не только размеров, поворота, положения, но и цвета. Попробуйте поменять цвет глобуса вот так:
@@ -150,6 +174,7 @@ struct ContentView: View {
```
![Анимация с изменением цвета](/lessons-extended-program/20.sprint/assets/img/l2_gif04.gif)
*Анимация с изменением цвета*
А если, предположим, мы хотим, чтобы view появлялось из ниоткуда? Это тоже можно осуществить с помощью свойства `opacity`.
@@ -170,6 +195,7 @@ struct ContentView: View {
```
![Анимация, привязанная к onAppear](/lessons-extended-program/20.sprint/assets/img/l2_gif05.gif)
*Анимация, привязанная к onAppear*
Таким образом, если нас интересует только смена одного состояния на другое, с этой задачей прекрасно справляется модификатор `animation`. Этот способ называется анимацией в неявном (implicit) виде.
@@ -235,12 +261,14 @@ struct ContentView: View {
```
![Анимация с withAnimation](/lessons-extended-program/20.sprint/assets/img/l2_gif06.gif)
*Анимация с withAnimation*
Как видите, здесь анимация затрагивает два объекта одновременно: появляется VStack с картинкой и надписью, а кнопка исчезает. Оба действия синхронизированы, поскольку запускаются одним и тем же триггером. Будь у нас больше объектов, пришлось бы к каждому добавлять модификаторы анимации и следить, чтобы все анимации имели одинаковые параметры.
Впрочем, это не означает, что функция `withAnimation()` всегда лучше, чем модификатор `animation`. Проведём эксперимент: заменим вид анимации `.bouncy` на `.easeInOut(duration: 3)`.
![Анимация с easeInOut](/lessons-extended-program/20.sprint/assets/img/l2_gif07.gif)
*Анимация с easeInOut*
Что мы видим: картинка и надпись уже появились и вращаются, а надпись «Старт» всё ещё не исчезла. Очевидно, мы хотели бы разделить эти анимации, а у кнопки выставить меньшую длительность. Это можно сделать, убрав `withAnimation` и поставив модификатор `animation` отдельно к каждому из двух объектов.
@@ -303,6 +331,7 @@ struct ContentView: View {
```
![Анимация easeInOut](/lessons-extended-program/20.sprint/assets/img/l2_gif08.gif)
*Анимация easeInOut*
Разницу между `linear` и `easeInOut` заметить вживую не так-то просто. Отличие только в небольшом замедлении в начале и в конце. Лучше всего это видно на графике: он показывает, как движется анимация с течением времени.
@@ -317,6 +346,7 @@ struct ContentView: View {
```
![Анимация interpolatingSpring](/lessons-extended-program/20.sprint/assets/img/l2_gif09.gif)
*Анимация interpolatingSpring*
Здесь анимация проходит затухающими волнами, которые изначально значительно выходят за рамки заданных значений, это видно по `scaleEffect` и по цвету картинки.
@@ -327,6 +357,7 @@ struct ContentView: View {
```
![Анимация spring](/lessons-extended-program/20.sprint/assets/img/l2_gif10.gif)
*Анимация spring*
Таким образом, просто меняя тип анимации, мы можем добиться самых разных визуальных эффектов относительно малыми усилиями.
@@ -334,7 +365,7 @@ struct ContentView: View {
Теперь оставим в покое модификатор `animation` — уберите его.
Вернёмся практически к тому, с чего начали и вновь поговорим об анимации жестов.
Вернёмся практически к тому, с чего начали, и вновь поговорим об анимации жестов.
```swift
struct ContentView: View {
@@ -372,16 +403,44 @@ struct ContentView: View {
```
![Перетаскивание](/lessons-extended-program/20.sprint/assets/img/l2_gif11.gif)
*Перетаскивание*
Как видите, анимация перетаскивания работает сама по себе, «из коробки».
Так же работает жест растягивания, который пригодится вам в любой галерее изображений:
С жестом растягивания чуть сложнее: с iOS 17 он коренным образом поменялся. В новой версии появляется новый property wrapper для хранения состояния: `@GestureState`.
```swift
@State var scale: CGFloat = 1.0 // тут будем хранить коэффициент увеличения
@GestureState var scale: CGFloat = 1.0 // тут будем хранить коэффициент увеличения
var magnification: some Gesture {
MagnifyGesture()
.updating($scale) { value, gestureState, transaction in
gestureState = value.magnification
}
}
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.blue)
Text("Привет, Практикум!")
}
.padding()
.scaleEffect(scale)
.gesture(magnification)
}
```
Добавим модификаторы:
> Обратите внимание: если мы планируем переиспользовать модификатор или он просто разросся в сложную конструкцию, удобно вынести его в отдельный объект.
На случай, если вам придётся поддерживать более ранние версии iOS, вот как это работало раньше:
```swift
@State var scale: CGFloat = 1.0
```
А вот так выглядели модификаторы:
```swift
.scaleEffect(scale)
@@ -393,9 +452,10 @@ struct ContentView: View {
)
```
![Растягивание](/lessons-extended-program/20.sprint/assets/img/l2_gif12.gif)
Это знание может вам понадобиться, поскольку в iOS 17 и далее старый вариант просто не работает, и вам надо будет поставить проверку, в какой именно версии iOS вы работаете, с помощью `available`.
И снова не требуется прописывать анимацию дополнительно — всё уже сделано за нас.
![Растягивание](/lessons-extended-program/20.sprint/assets/img/l2_gif12.gif)
*Растягивание*
КНОПКА
Отлично!
@@ -405,6 +465,7 @@ struct ContentView: View {
В прошлом уроке мы рисовали «цветок» из нескольких кругов. Попробуйте анимировать его, как в приложении «Дыхание» на Apple Watch: в начальном положении все круги расположены друг на другом, а в финальном — увеличивается, превращаются в цветок, одновременно совершая поворот.
![Финальная анимация](/lessons-extended-program/20.sprint/assets/img/l2_gif13.gif)
*Финальная анимация*
КНОПКА
Авторское решение
@@ -1,8 +1,8 @@
# Сложные анимации
Привет! В предыдущих уроках вы познакомились с основами анимации в SwiftUI: анимацией свойств и при помощи жестов. Но этим тема не исчерпывается. Что, если мы захотим запустить несколько анимаций одновременно? А если одну за другой? А если не сразу, а после паузы? Более сложные задачи требуют более сложных подходов.
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
![img](https://github.com/Yandex-Practicum/mobile-iOS/blob/main/lessons-extended-program/20.sprint/01.%20Анимации%20в%20SwiftUI/pictures_in_color/урок%203_сложные%20задачи.png)
Привет! В предыдущих уроках вы познакомились с основами анимации в SwiftUI: анимацией свойств и при помощи жестов. Но этим тема не исчерпывается. Что, если мы захотим запустить несколько анимаций одновременно? А если одну за другой? А если не сразу, а после паузы? Более сложные задачи требуют более сложных подходов.
Поэтому сегодня в уроке мы затронем разные варианты сложных анимаций:
@@ -78,6 +78,7 @@ struct ContentView: View {
Теперь анимация повторяется бесконечно:
![И снова, и снова...](/lessons-extended-program/20.sprint/assets/img/l3_gif02.gif)
*И снова, и снова...*
КНОПКА
Прекрасно!
@@ -87,6 +88,7 @@ struct ContentView: View {
В коде вы столкнулись с параметром модификатора repeatForever, который отвечает за автоматический возврат в исходное состояние — autoreverses. Чтобы разобраться, как он работает, немного поиграем с параметрами. Поменяйте значение `autoreverses` на `true`.
![Анимация с autoreverses: true](/lessons-extended-program/20.sprint/assets/img/l3_gif03.gif)
*Анимация с autoreverses: true*
Посмотрите, как изменился результат: теперь это не «полёт в пространство», а, скорее, «пульсация». Такая анимация может пригодиться, к примеру, в приложениях здоровья, если вы захотите показать там изображение бьющегося сердца, или для привлечения внимания к какому-нибудь элементу на экране.
@@ -107,12 +109,14 @@ struct ContentView: View {
}
```
![Три повтора](/lessons-extended-program/20.sprint/assets/img/l3_gif04.gif)
*Три повтора*
Анимация появилась на экране три раза.
Для эксперимента укажем теперь `autoreverses: true`:
![Полтора повтора](/lessons-extended-program/20.sprint/assets/img/l3_gif05.gif)
*Полтора повтора*
Здесь анимация повторяется тоже три раза, но это не три пульсации, а как бы полторы: анимация в состоянии 2, возврат в 1 и снова в 2.
@@ -162,6 +166,7 @@ Circle()
```
![Две окружности... но вторую не видно](/lessons-extended-program/20.sprint/assets/img/l3_gif06.gif)
*Две окружности... но вторую не видно*
На вид ничего не изменилось, потому что две окружности наложились друг на друга и анимируются синхронно. Чтобы развести их во времени, задержим одну из анимаций на половину длительности, то есть на секунду.
@@ -212,6 +217,7 @@ struct ContentView: View {
Поведение анимации явно отличается от того, что мы хотели увидеть:
![Очень странно выглядит...](/lessons-extended-program/20.sprint/assets/img/l3_gif07.gif)
*Очень странно выглядит...*
Начало выглядит правильным, но потом анимация начинает будто запинаться. Дело в том, что за синхронизацию анимации разных View отвечала как раз функция `withAnimation`.
@@ -221,7 +227,7 @@ struct ContentView: View {
Этот способ появился в iOS 17, и называется он `phaseAnimator`.
> О том, чем мы богаты в предыдущих версиях и что можем использовать вместо `phaseAnimator`, коротко поговорим дальше.
> О том, чем мы богаты в предыдущих версиях и что можем использовать вместо `phaseAnimator`, коротко поговорим дальше на случай, если вам доведётся работать с приложением, где поддерживаются более ранние версии.
`phaseAnimator` — это механизм, который позволяет нам создавать многоступенчатые анимации. Мы можем задать перечень фаз и для каждой из них описать состояние View. Для этого создадим enum, в котором укажем, что у анимации всего два состояния: начальное и конечное.
@@ -282,6 +288,7 @@ struct ContentView: View {
```
![Вот теперь всё отлично!](/lessons-extended-program/20.sprint/assets/img/l3_gif08.gif)
*Вот теперь всё отлично!*
Остановимся подробнее на строке:
@@ -374,6 +381,7 @@ struct ContentView: View {
Здесь все параметры, включая анимацию, мы взяли из enum. Вот такой забавный результат получился:
![Все три фазы чётко видны](/lessons-extended-program/20.sprint/assets/img/l3_gif09.gif)
*Все три фазы чётко видны*
У каждой фазы свой тип анимации, свои параметры, и всё это работает плавно и бесшовно.
@@ -408,6 +416,7 @@ struct ContentView: View {
Вот такой эффект получается в итоге:
![Те же три фазы, но без повтора](/lessons-extended-program/20.sprint/assets/img/l3_gif10.gif)
*Те же три фазы, но без повтора*
Анимация проходит здесь те же фазы, но останавливается, поскольку таким методом анимацию не зациклить. Однако это и не всегда нужно.
@@ -442,6 +451,7 @@ struct ContentView: View {
Сверстайте анимацию: из-за левого и правого края экрана по отдельности появляются красные слова «Привет,» и «Практикум!», одновременно из прозрачных становясь непрозрачными. Когда они доходят до центра и становятся друг над другом, их цвет меняется на зелёный.
![Примерно так должен выглядеть результат](/lessons-extended-program/20.sprint/assets/img/l3_gif11.gif)
*Примерно так должен выглядеть результат*
СКРЫВАШКА
**Авторское решение**
@@ -482,19 +492,25 @@ struct ContentView: View {
**КВИЗ - НАЧАЛО**
1. Чтобы немедленно повторить анимацию объекта, лучше всего…
1. Для создания повторяющейся определённое количество раз или бесконечно анимации, какой из следующих подходов является наиболее предпочтительным?
[x] Добавить модификатор .repeatForever.
Да, он именно для этого и нужен.
[ ] Использовать `Timer` в сочетании с `@State` переменной для периодического запуска анимации через `withAnimation`.
Хотя это и рабочий способ, он требует ручного управления таймером и состоянием, что менее декларативно по сравнению со встроенными средствами SwiftUI для повторения анимаций.
[ ] Добавить модификатор .delay, чтобы отложить следующий круг анимации.
Нет, модификатор .delay отложит всю анимацию целиком.
[x] Применить к объекту модификатор `.repeatForever`.
Этот модификатор предоставляет эффективный способ управления повторением анимации непосредственно на уровне её определения.
[x] Добавить модификатор .repeatCount.
Всё верно, этот модификатор отвечает за повторение.
[ ] Обернуть изменение состояния в `withAnimation` и использовать его параметр `completion` для рекурсивного вызова функции, перезапускающей анимацию.
Параметр `completion` предназначен для выполнения действий *после* завершения анимации, а не для её непосредственного повторения. Такой подход усложнит код и не является стандартным решением для циклической анимации.
[ ] Поставить таймер и запустить анимацию ещё раз.
Теоретически так сделать можно. Но высчитать правильный момент сложно, поэтому делать так не стоит.
[ ] Использовать `DispatchQueue.main.asyncAfter` для планирования повторного изменения анимируемого состояния после завершения предыдущей анимации.
Этот метод требует точного расчёта длительности анимации и может привести к неточностям или сложности в синхронизации, особенно при изменении параметров анимации.
[x] Применить к объекту модификатор `.repeatCount`.
Этот модификатор предоставляет эффективный способ управления повторением анимации непосредственно на уровне её определения.
[ ] В `phaseAnimator` определить последовательность фаз, которая циклически возвращается к начальной фазе, тем самым имитируя повторение.
`phaseAnimator` предназначен для сложных многоступенчатых анимаций. Хотя с его помощью можно добиться повторения, для простого циклического воспроизведения одной и той же анимации это избыточное решение по сравнению с `.repeatForever` или `.repeatCount`.
2. Чтобы запустить следующую анимацию из цепочки, мы можем…
@@ -515,7 +531,7 @@ struct ContentView: View {
**КВИЗ - КОНЕЦ**
# Подведём итоги
## Подведём итоги
Вы научились запускать повтор анимации, откладывать её, запускать несколько анимаций параллельно и последовательно. Отлично! Это значит, что теперь вы сможете не только сделать анимированную заставку или «живой» плейсхолдер, когда грузятся данные, но и красиво обработать дальнейшие смены состояния. А анимация как раз и нужна, чтобы всё было красиво.
@@ -1,3 +1,7 @@
# Анимации переходов
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В предыдущих уроках вы познакомились с основными аспектами работы с анимацией:
- применяли анимации изменения размера, поворота, смещения и других параметров view;
@@ -43,6 +47,7 @@ struct ContentView: View {
```
![Появление нового view — пока без анимации](/lessons-extended-program/20.sprint/assets/img/l4_gif01.gif)
*Появление нового view — пока без анимации*
Здесь переменная `showNewView` отвечает за появление и исчезновение надписи. Если сейчас нажать на иконку глобуса, то можно увидеть, что надпись появляется одномоментно, а глобус буквально выпрыгивает из-под пальцев у пользователя. Последнее — это ошибка, которую часто допускают новички, и хоть она не связана с анимацией напрямую, нелишне будет напомнить: то, что находится непосредственно под пальцами у пользователя, должно вести себя максимально предсказуемо!
@@ -73,6 +78,7 @@ struct ContentView: View {
```
![Глобус больше не прыгает](/lessons-extended-program/20.sprint/assets/img/l4_gif02.gif)
*Глобус больше не прыгает*
> Это не единственный способ зафиксировать положение Image. Если бы, к примеру, текст был длинный, и мы не знали его высоту заранее, пришлось бы использовать GeometryReader или разносить Image и Text на разные слои. Но в этом случае нам хватит и простого варианта.
@@ -98,9 +104,10 @@ Text("Привет, Практикум!")
}
```
> Здесь и далее специально указана большая длительность анимации (целых три секунды!), чтобы детально рассмотреть, что происходит. В реальной работе хронометраж анимации встречается редко.
> Здесь и далее специально указана большая длительность анимации (целых три секунды!), чтобы детально рассмотреть, что происходит. В реальной работе такой хронометраж анимации встречается редко, речь чаше всего о долях секунды.
![Анимация transition(.opacity)](/lessons-extended-program/20.sprint/assets/img/l4_gif03.gif)
*Анимация transition(.opacity)*
КНОПКА-ДИАЛОГ
Студент: А это не та же анимация свойства opacity из второго урока?
@@ -121,6 +128,7 @@ Text("Привет, Практикум!")
```
![Анимация .transition(.slide)](/lessons-extended-program/20.sprint/assets/img/l4_gif04.gif)
*Анимация .transition(.slide)*
Схожим образом работает и вариант `push`. Здесь мы должны указать, с какой стороны должно «приехать» view. Предположим, снизу.
@@ -131,6 +139,7 @@ Text("Привет, Практикум!")
```
![Анимация .transition(.push(from: .bottom))](/lessons-extended-program/20.sprint/assets/img/l4_gif05.gif)
*Анимация .transition(.push(from: .bottom))*
Вариант `move` отличается от `push` не так уж сильно:
@@ -140,12 +149,13 @@ Text("Привет, Практикум!")
.transition(.move(edge: .bottom))
```
![Анимация .transition(.move(edge: .bottom))](/lessons-extended-program/20.sprint/assets/img/l4_gif06.gif)
*Анимация .transition(.move(edge: .bottom))*
Различие между всеми этими вариантами — в разных исходных точках и разном изменении скорости на протяжении анимации.
### Асимметричный переход
В примерах выше анимация была симметричная — каким образом view появлялось, таким же и исчезало. Можно, однако, задать разное поведение — для этого мы используем вариант `asymmetric`. Это может пригодиться, если ваш дизайнер придумал две разные анимации: например, подсказка выезжает из-за края экрана, а потом исчезает, уменьшаясь в точку.
В примерах выше анимация была симметричная — каким образом view появлялось, таким же и исчезало. Можно, однако, задать разное поведение — для этого мы используем вариант `asymmetric`. Это может пригодиться, если ваш дизайнер придумал две разные анимации: например, подсказка выезжает из-за края экрана, а на втором тапе на глобус исчезает, уменьшаясь в точку.
```swift
Text("Привет, Практикум!")
@@ -154,10 +164,11 @@ Text("Привет, Практикум!")
```
![Анимация .transition(.asymmetric(insertion: .slide, removal: .scale))](/lessons-extended-program/20.sprint/assets/img/l4_gif07.gif)
*Анимация .transition(.asymmetric(insertion: .slide, removal: .scale))*
## Комбинирование анимаций
Модификатор `transition` позволяет не только использовать любой из встроенных вариантов анимации перехода, но и комбинировать их.
Модификатор `transition` позволяет не только использовать любой из встроенных вариантов анимации перехода, но и комбинировать их. Например, мы хотим, чтобы view появлялось снизу, из размытия и по пути меняло размер.
```swift
Text("Привет, Практикум!")
@@ -166,6 +177,7 @@ Text("Привет, Практикум!")
```
![Анимация .transition(.move(edge: .bottom).combined(with: .blurReplace).combined(with: .scale))](/lessons-extended-program/20.sprint/assets/img/l4_gif08.gif)
*Анимация .transition(.move(edge: .bottom).combined(with: .blurReplace).combined(with: .scale))*
Таким образом мы можем добавить любое число анимаций — их количество ограничено лишь здравым смыслом и производительностью.
@@ -176,6 +188,7 @@ Text("Привет, Практикум!")
Например, мы хотим, чтобы надпись при появлении поворачивалась по оси z, то есть как бы возникала перпендикулярно плоскости экрана и приходила в нормальное положение.
![Анимация с переворотом](/lessons-extended-program/20.sprint/assets/img/l4_gif09.gif)
*Анимация с переворотом*
Для этого создадим объект класса GeometryEffect, где опишем, что должно происходить с view.
@@ -228,10 +241,10 @@ Text("Привет, Практикум!")
1. Какими из перечисленных способов можно заставить view появиться на экране с левой стороны?
[x] .transition(.slide)
Пояснение: Верно! С этим модификатором view плавно появляется слева.
Пояснение: С этим модификатором view плавно появляется слева.
[x] .transition(.move(.edge(.leading)))
Пояснение: Правильно! С параметром `move` в сочетании с `leading` view появится именно слева.
Пояснение: С параметром `move` в сочетании с `leading` view появится именно слева.
[ ] .transition(.push(.edge(.trailing)))
Пояснение: Конечно, мы можем использовать параметр `push`, чтобы view появилось на экране, но из-за `trailing` оно выедет не слева, а справа.
@@ -245,13 +258,13 @@ Text("Привет, Практикум!")
2. Какими из перечисленных способов можно сделать так, чтобы одна анимация запустилась сразу после другой?
[x] Использовать phaseAnimator (начиная с iOS 17)
Пояснение: Верно, он для этого и придуман.
Пояснение: phaseAnimator для этого и придуман.
[x] Установить параметр анимации `autoreverse: true`.
Пояснение: Да! Параметр `autoreverse: true` сразу после заданной анимации запускает обратную к заданной.
[x] Запустить вторую анимацию с параметром delay — то есть на заданное время позже.
Пояснение: И снова верно! Это трудоёмкий способ, но по-прежнему эффективный.
Пояснение: Это трудоёмкий способ, но по-прежнему эффективный.
[ ] К первой анимации дописать модификатор `combined`, где в качестве параметра указать вторую анимацию.
Пояснение: Во-первых, `combined` есть исключительно у анимации переходов; а во-вторых, он создаст не последовательные анимации, а одновременные.
@@ -262,18 +275,19 @@ Text("Привет, Практикум!")
[x] В конце анимации вызвать замыкание, в котором запустить следующую анимацию.
Пояснение: Да. Это один из немногих способов эффективно решить эту задачу до iOS 17.
3. С модификатором .transition(.slide) view по умолчанию появляется слева. Как сделать так, чтобы оно исчезало вправо?
[ ] Указать асимметричную анимацию перехода: на вставку — slide, на исчезновение — move(.edge: .trailing)
Пояснение: Вообще говоря, это сработает. Но есть вариант лучше и проще.
3. Если view использует `.transition(.slide)` для появления, как оно будет вести себя при исчезновении?
[ ] Задать offset, который будет увеличиваться при исчезновении view.
Пояснение: Нет, offset не поменяет направление движения. Можно, конечно, применить очень сложные формулы расчёта, но это верный способ получить непредсказуемое поведение.
[ ] Оно исчезнет, двигаясь влево (к тому же краю, откуда появилось).
Пояснение: Стандартное поведение `.slide` для исчезновения — движение к противоположному краю от стандартного появления.
[ ] Указать параметр анимации `autoreverse: true`.
Пояснение: Увы, `autoreverse: true` тут ничем не поможет.
[x] Оно исчезнет, двигаясь к противоположному краю от появления.
Пояснение: По умолчанию `.transition(.slide)` заставляет view появляться с ведущего края (leading) и исчезать к ведомому краю (trailing).
[x] Не надо ничего делать. .transition(.slide) по умолчанию как раз исчезает вправо.
Пояснение: Совершенно верно! В slide асимметрия заложена изначально.
[ ] Оно исчезнет с эффектом изменения прозрачности (`.opacity`).
Пояснение: `.slide` отвечает за анимацию смещения. Для анимации прозрачности используется `.transition(.opacity)`.
[ ] Его поведение при исчезновении не определено и требует явного указания через `.asymmetric`, иначе оно просто исчезнет без анимации смещения.
Пояснение: `.transition(.slide)` имеет чётко определённое поведение по умолчанию как для появления, так и для исчезновения.
**КВИЗ - КОНЕЦ**
@@ -1,11 +1,11 @@
# Вспоминаем основы Combine
В 16-м спринте вы познакомились с фреймворком Apple Combine — мощным инструментом для обработки асинхронных событий в iOS-приложениях.
Combine позволяет эффективно работать с асинхронными потоками данных и таким образом обеспечивать чистый управляемый код. Этот фреймворк находит своё применение в различных аспектах разработки: начиная от выполнения сетевых запросов, которые требуют обработки данных, поступающих асинхронно от сервера, и заканчивая управлением пользовательским интерфейсом, где требуется отображать данные в реальном времени или реагировать на взаимодействие пользователя.
Применение Combine упрощает реализацию задач комбинирования, фильтрации, преобразования и агрегирования потоков данных. При этом он обеспечивает производительность и отзывчивость приложения.
![img](https://github.com/Yandex-Practicum/mobile-iOS/blob/main/lessons-extended-program/20.sprint/02.%20SwiftUI%20%2B%20Combine.%20Практика/pictures_in_color/урок01_регулировщик%20потоков.png)
Фреймворк предлагает разработчикам декларативный Swift API для работы с асинхронными событиями, используя концепции издателей (publishers) и подписчиков (subscribers). Это позволяет создавать более простые потоки данных, автоматически управлять жизненным циклом подписок и избавляться от необходимости написания шаблонного кода.
В этой теме мы будем использовать Combine для взаимодействия с асинхронными событиями таймера при создании экрана Stories. Но прежде немного освежим знания по работе с Combine с помощью квизов.
@@ -58,83 +58,178 @@ Sink (`sink`) — это метод, который является конеч
Фрагменты кода:
```swift
A. let publisher = Just("Hello world").map { $0.uppercased() }
A.
let sourceA = PassthroughSubject<Int, Error>()
let resultA = sourceA
.flatMap { value -> AnyPublisher<String, Error> in
if value < 0 {
return Fail(error: NSError(domain: "NegativeError", code: -1)).eraseToAnyPublisher()
}
return Just("Processed: \(value)").setFailureType(to: Error.self).eraseToAnyPublisher()
}
.catch { _ in Just("Handled Error") }
```
B. let subject = PassthroughSubject<String, Never>()
```swift
B.
let intStream = PassthroughSubject<Int, Never>()
let stringStream = CurrentValueSubject<String, Never>("Prefix")
let resultB = intStream
.combineLatest(stringStream)
.map { number, prefixText -> String in
"\(prefixText): \(number * number)"
}
```
C. publisher.sink { print($0) }
```swift
C.
let inputC = PassthroughSubject<String, Never>()
let resultC = inputC
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
```
D. let cancellable = publisher.filter { $0.count > 5 }.sink { print($0) }
```swift
D.
let numbersD = (1...10).publisher
let resultD = numbersD
.filter { $0 % 2 != 0 }
.map { $0 * 3 }
.collect(2)
```
```swift
E.
enum CustomError: Error { case conversionFailed, unknown }
let stringValuesE = PassthroughSubject<String, CustomError>()
let resultE = stringValuesE
.tryMap { stringValue -> Int in
guard let intValue = Int(stringValue) else { throw CustomError.conversionFailed }
return intValue
}
.mapError { error -> CustomError in
return (error as? CustomError) ?? .unknown
}
.replaceError(with: 0)
```
Описания:
1. Подписывается на издателя и печатает все выданные значения.
2. Сущность, которая выдаёт строки и никогда не генерирует ошибок.
3. Издатель, который преобразует единственное выданное значение в верхний регистр.
4. Фильтрует выданные значения и печатает только те, у которых более 5 символов.
1. Преобразует каждое входящее целое число, создавая нового издателя строк. Если число отрицательное, инициируется ошибка, которая затем перехватывается и заменяется на сообщение об ошибке.
2. Объединяет последние значения из потока целых чисел и потока строк, который имеет начальное значение. Результатом является новая строка, сформированная из текстового префикса и квадрата числа.
3. Обрабатывает поток строкового ввода, применяя задержку перед передачей данных, устраняя дублирующиеся последовательные значения и исключая пустые строки из потока.
4. Извлекает из числовой последовательности только нечётные числа, утраивает каждое из них, а затем собирает обработанные числа в массивы по два элемента.
5. Пытается конвертировать строковые значения в целочисленные. В случае неудачи конвертации, типизирует возникшую ошибку и затем заменяет любую ошибку в потоке на значение по умолчанию (0).
Ответ:
A — 3, B — 2, C — 1, D — 4
A — 1, B — 2, C — 3, D — 4, E — 5
Объяснения:
Фрагмент кода A: Создаёт издателя, который выдаёт единственное значение Hello world и преобразует его в верхний регистр.
Фрагмент кода B: Создаёт субъект, который выдаёт строки и никогда не генерирует ошибки.
Фрагмент кода C: Подписывается на издателя и печатает все выданные значения.
Фрагмент кода D: Подписывается на издателя, фильтрует выданные значения, чтобы оставить только те, у которых более 5 символов, и печатает их.
Тут мы рассмотрели различные операторы Combine и их применение. Фрагмент A демонстрирует использование `flatMap` для асинхронного преобразования чисел в строки, где для отрицательных чисел генерируется ошибка с помощью `Fail`, которая затем перехватывается оператором `catch` и заменяется на строку "Handled Error". Фрагмент B показывает, как `combineLatest` объединяет последние значения из двух потоков (`intStream` и `stringStream`), причём `CurrentValueSubject` (`stringStream`) предоставляет начальное значение "Prefix", а `map` формирует итоговую строку, используя текстовый префикс и квадрат числа. Для обработки пользовательского ввода в фрагменте C применяется цепочка операторов: `debounce` вносит задержку, пропуская значения только после паузы, `removeDuplicates` устраняет повторяющиеся подряд значения, а `filter` исключает пустые строки. Фрагмент D иллюстрирует работу с последовательностью чисел: `filter` отбирает нечётные числа, `map` утраивает каждое из них, а `collect(2)` группирует обработанные числа в массивы по два элемента. Наконец, фрагмент E показывает обработку потенциальных ошибок: `tryMap` пытается преобразовать строки в `Int` и может выбросить ошибку `CustomError.conversionFailed`, `mapError` затем типизирует эту ошибку или возвращает `.unknown`, а `replaceError(with: 0)` заменяет любую ошибку в потоке на значение по умолчанию `0`.
### 3. Понимание кода
Инструкция: Выберите фрагмент кода, который правильно выполняет описанную задачу с использованием Combine.
Выберите фрагмент кода, который подписывается на издателя, применяет оператор к выданным значениям, а затем печатает эти значения без изменений.
Инструкция: Внимательно изучите варианты кода. Какой из них корректно использует оператор `map` для преобразования значения, после чего результат этого преобразования выводится в консоль без дополнительных изменений в блоке `sink`?
Варианты:
```swift
A. let publisher = Just("Hello").sink { print($0.lowercased()) }
[ ]
let publisherA = Just(5)
publisherA.sink { value in
print(value * 2)
}
```
Пояснение: Преобразование (`value * 2`) происходит непосредственно внутри блока `sink`. Оператор `map` не используется для этого преобразования перед `sink`.
B. let publisher = Just("Hello").map { $0.lowercased() }.sink { print($0) }
```swift
[x]
let publisherB = Just(5)
publisherB
.map { $0 * 2 }
.sink { value in
print(value)
}
```
Пояснение: Сначала `Just(5)` выдаёт значение 5. Затем оператор `map { $0 * 2 }` преобразует это значение в 10. После этого `sink { value in print(value) }` получает преобразованное значение 10 и выводит его без дальнейших изменений внутри самого блока `sink`.
C. PassthroughSubject<String, Never>().sink { print($0) }
```swift
[ ]
let publisherC = Just(5)
publisherC
.map { $0 * 2 }
.sink { value in
print(value + 1)
}
```
Пояснение: Хотя оператор `map` используется для преобразования 5 в 10, значение (10) затем дополнительно изменяется (`value + 1`) внутри блока `sink` перед выводом. Это не соответствует условию.
D. Just("Hello").filter { $0.count > 3 }.sink { print($0.lowercased()) }
```swift
[ ]
let publisherD = Just(5)
publisherD
.filter { $0 > 0 }
.sink { value in
print(value * 2)
}
Пояснение: Используется оператор `filter`, который предназначен для **отбора** значений, а не для их **преобразования**, как `map`. Кроме того, основное арифметическое преобразование (`value * 2`) также происходит внутри `sink`, что не верно.
```
Правильный ответ: B (Этот фрагмент кода создаёт издателя, который выдаёт одно значение, применяет преобразование к нижнему регистру и затем использует `sink` для печати преобразованного значения.)
### 4. Сопоставление описаний и кода
Объяснения:
Вариант А: В этом случае фрагмент не применяет оператор к выданным значениям, но перед печатью преобразует значение в нижний регистр.
Вариант С: Этот вариант не применяет оператор к выданным значениям, а просто печатает их.
Вариант D: преобразует значения перед печатью, тогда как по условию требуется печать без изменений.
### 4. Сопоставление диаграммы и кода
Инструкция: Сопоставьте следующие описания операций Combine с соответствующим фрагментом кода.
Инструкция: Сопоставьте следующие описания издателей Combine с соответствующим фрагментом кода
Описания:
1. Издатель, который выдаёт начальное значение и затем обновляет его, когда поступают новые значения.
2. Издатель, который фильтрует строки на основе их длины, а затем собирает их в массив.
1. Хранит и публикует последнее отправленное значение новым подписчикам, а также начальное значение при первой подписке. Позволяет программно отправлять новые значения.
2. Отбирает элементы по условию, преобразует каждый отобранный элемент и затем собирает все преобразованные элементы в один массив, который публикуется как единичное значение.
3. Комбинирует последние значения из двух различных издателей и выдаёт эти значения каждый раз, когда любой из исходных издателей публикует новое значение (после того как все исходные издатели опубликовали хотя бы по одному значению).
4. Объединяет выходные данные от нескольких издателей одного типа в единый поток событий, передавая их по мере поступления без изменений или дополнительной обработки самих значений.
5. Ожидает определённую паузу в потоке входящих событий от источника перед тем, как передать последнее полученное событие дальше, эффективно отсеивая слишком частые обновления.
Фрагменты кода:
```swift
A. let publisher = CurrentValueSubject<String, Never>("Initial").sink { print($0) }
A.
let currentData = CurrentValueSubject<String, Never>("Начальное значение")
let cancellableA = currentData.sink { print("\($0)") }
currentData.send("Обновлённое значение")
B. let publisher = ["Short", "Very long string"].publisher.filter { $0.count > 5 }.collect().sink { print($0) }
B.
let dataStream = (1...7).publisher
.filter { $0 % 2 != 0 }
.map { "Число: \($0)" }
.collect()
let cancellableB = dataStream.sink { print("\($0)") }
C.
let intSource = PassthroughSubject<Int, Never>()
let stringSource = PassthroughSubject<String, Never>()
let combinedStream = Publishers.CombineLatest(intSource, stringSource).map { "Последние: \($0) и '\($1)'" }
let cancellableC = combinedStream.sink { print("\($0)") }
D.
let sourceAlpha = PassthroughSubject<Int, Never>()
let sourceBeta = PassthroughSubject<Int, Never>()
let mergedStream = Publishers.Merge(sourceAlpha, sourceBeta)
let cancellableD = mergedStream.sink { print("\($0)") }
E.
let userInput = PassthroughSubject<String, Never>()
let debouncedInput = userInput
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
let cancellableE = debouncedInput.sink { print("\($0)") }
```
Ответ:
1 — A, 2 — B
1 — A, 2 — B, 3 — C, 4 — D, 5 — E
Объяснения:
Фрагмент кода A: `CurrentValueSubject` — это издатель, который выдаёт начальное значение (в данном случае Initial) и затем обновляет его, когда поступают новые значения.
Фрагмент кода B: Метод `publisher` преобразует массив строк в издателя; затем оператор `filter` фильтрует строки на основе их длины, и, наконец, оператор `collect` собирает отфильтрованные строки в массив.
Тут мы рассмотрели ключевые издатели и операторы Combine. Фрагмент A иллюстрирует `CurrentValueSubject`, который хранит и публикует последнее отправленное значение, включая начальное, новым подписчикам и позволяет программно отправлять обновления. Фрагмент B демонстрирует создание издателя из последовательности чисел с последующей обработкой: оператор `filter` отбирает элементы по условию "нечётные числа", `map` преобразует каждый отобранный элемент в строку, и `collect` собирает все преобразованные элементы в один массив, публикуемый как единичное значение. Фрагмент C использует `Publishers.CombineLatest` для комбинирования последних значений из двух различных издателей; он выдаёт эти значения в виде кортежа каждый раз, когда любой из исходных издателей публикует новое значение (после того как все исходные издатели опубликовали хотя бы по одному значению), а `map` затем преобразует этот кортеж в строку. Для объединения выходных данных от нескольких издателей одного типа в единый поток событий, передавая их по мере поступления без изменений, используется `Publishers.Merge`, как показано в фрагменте D. Наконец, фрагмент E демонстрирует `PassthroughSubject` для имитации потока ввода, где оператор `debounce` ожидает определённую паузу в потоке входящих событий перед передачей последнего полученного события, эффективно отсеивая слишком частые обновления, а `removeDuplicates()` дополнительно оптимизирует поток, удаляя повторяющиеся значения.
### 5. Определите оператор Combine
@@ -147,23 +242,16 @@ B. let publisher = ["Short", "Very long string"].publisher.filter { $0.count > 5
Варианты:
```swift
A. [1, 2, 3, 4, 5].publisher.map { $0 * 2 }.filter { $0 % 2 == 0 }.collect().sink { print($0) }
B. [1, 2, 3, 4, 5].publisher.filter { $0 % 2 == 0 }.map { $0 * 2 }.sink { print($0) }
C. [1, 2, 3, 4, 5].publisher.flatMap { Just($0 * 2) }.filter { $0 % 2 == 0 }.collect().sink { print($0) }
D. [1, 2, 3, 4, 5].publisher.map { $0 * 2 }.collect().filter { $0 % 2 == 0 }.sink { print($0) }
[x] [1, 2, 3, 4, 5].publisher.map { $0 * 2 }.filter { $0 % 2 == 0 }.collect().sink { print($0) }
Пояснение: A. (Этот вариант применяет операцию `map` для умножения каждого числа на 2, фильтрует результаты, чтобы включать только чётные числа с помощью `filter`, и, наконец, собирает результаты в массив перед их печатью.)
[ ] [1, 2, 3, 4, 5].publisher.filter { $0 % 2 == 0 }.map { $0 * 2 }.sink { print($0) }
Пояснение: Порядок операторов `map` и `filter` неверен: сначала нужно умножить числа, а затем отфильтровать их.
[ ] [1, 2, 3, 4, 5].publisher.flatMap { Just($0 * 2) }.filter { $0 % 2 == 0 }.collect().sink { print($0) }
Пояснение: Здесь используется `flatMap` и выдаются значения типа `Just`, что не соответствует описанию.
[ ] [1, 2, 3, 4, 5].publisher.map { $0 * 2 }.collect().filter { $0 % 2 == 0 }.sink { print($0) }
Пояснение: Оператор `collect` должен быть вызван после `filter`, чтобы собрать отфильтрованные значения в массив.
```
Правильный ответ: A. (Этот вариант применяет операцию `map` для умножения каждого числа на 2, фильтрует результаты, чтобы включать только чётные числа с помощью `filter`, и, наконец, собирает результаты в массив перед их печатью.)
Объяснения:
Вариант B: Порядок операторов `map` и `filter` неверен: сначала нужно умножить числа, а затем отфильтровать их.
Вариант C: Здесь используется `flatMap` и выдаются значения типа `Just`, что не соответствует описанию.
Вариант D: Оператор `collect` должен быть вызван после `filter`, чтобы собрать отфильтрованные значения в массив.
**Описание 2:**
Издатель Combine, который публикует диапазон целых чисел; преобразует эти числа в строки, затем сохраняет только строки с количеством символов больше 1, и, наконец, печатает каждую оставшуюся строку.
@@ -171,52 +259,48 @@ D. [1, 2, 3, 4, 5].publisher.map { $0 * 2 }.collect().filter { $0 % 2 == 0 }.sin
Варианты:
```swift
A. (1...10).publisher.map { "\($0)" }.filter { $0.count > 1 }.sink { print($0) }
B. (1...10).publisher.filter { "\($0)".count > 1 }.map { "\($0)" }.sink { print($0) }
C. (1...10).publisher.map { "\($0)" }.sink { $0.count > 1 ? print($0) : nil }
D. (1...10).publisher.flatMap { Just("\($0)").filter { $0.count > 1 } }.sink { print($0) }
[x] (1...10).publisher.map { "\($0)" }.filter { $0.count > 1 }.sink { print($0) }
Пояснение: Этот вариант представляет издатель, где каждое целое число сначала преобразуется в строку, затем фильтруется, чтобы сохранить только те строки, которые имеют более одного символа, и, наконец, печатает.
[ ] (1...10).publisher.filter { "\($0)".count > 1 }.map { "\($0)" }.sink { print($0) }
Пояснение: Порядок операторов `filter` и `map` неверен: сначала нужно преобразовать числа в строки, а затем отфильтровать их.
[ ] (1...10).publisher.map { "\($0)" }.sink { $0.count > 1 ? print($0) : nil }
Пояснение: Здесь мы видим применение условного выражения внутри `sink`, что противоречит описанию.
[ ] (1...10).publisher.flatMap { Just("\($0)").filter { $0.count > 1 } }.sink { print($0) }
Пояснение: Этот вариант использует `flatMap` и выдаёт издателей типа `Just`, а не строки. Это не соответствует описанию.
```
Правильный ответ: A.
Этот вариант правильно представляет издатель, где каждое целое число сначала преобразуется в строку, затем фильтруется, чтобы сохранить только те строки, которые имеют более одного символа, и, наконец, печатает.
Объяснения:
Вариант B: Порядок операторов `filter` и `map` неверен: сначала нужно преобразовать числа в строки, а затем отфильтровать их.
Вариант C: Здесь мы видим применение условного выражения внутри `sink`, что противоречит описанию.
Вариант D: Этот вариант неверен, потому что он использует `flatMap` и выдаёт издателей типа `Just`, а не строки. Это не соответствует описанию.
### 6. Сопоставление диаграммы и кода
Инструкция: Выберите фрагмент кода, который точно представляет описываемую операцию Combine.
**Описание 1:**
Создайте издателя, который выдаёт последовательность целых чисел, использует оператор для преобразования каждого целого числа в квадрат этого целого числа, и затем печатает значения.
Выберите фрагмент кода, который создаёт издателя последовательности чисел от 1 до 5, добавляет единицу к каждому числу с помощью отдельного оператора Combine, а затем печатает результат.
```swift
A. (1...5).publisher.flatMap { Just($0 * $0) }.sink { print($0) }
[x]
(1...5).publisher
.map { $0 + 1 }
.sink { print($0) }
Пояснение: Корректно использует оператор `map` для преобразования (добавления единицы) каждого элемента перед печатью.
B. (1...5).publisher.map { $0 + $0 }.sink { print($0) }
[ ]
(1...5).publisher
.filter { $0 > 1 }
.sink { print($0 + 1) }
Пояснение: Использует оператор `filter` для отбора элементов, а добавление единицы происходит внутри `sink`, что не соответствует требованию использования оператора Combine для преобразования.
C. (1...5).publisher.flatMap { $0 * $0 }.sink { print($0) }
[ ]
(1...5).publisher
.sink { print($0 + 1) }
Пояснение: Выполняет добавление единицы внутри замыкания `sink`, а не с помощью отдельного оператора Combine для преобразования.
D. (1...5).publisher.map { Just($0 * $0) }.sink { print($0) }
[ ]
(1...5).publisher
.map { $0 * 2 }
.sink { print($0) }
Пояснение: Использует оператор `map`, но для умножения на 2, а не для добавления единицы.
```
Правильный ответ: С.
`flatMap` используется для преобразования каждого выдаваемого целого числа в нового издателя, который выдаёт квадрат числа, и эти значения затем печатаются.
Объяснения:
Вариант B удваивает число, а не возводит в квадрат.
Варианты A и D преобразуют целые числа в издатели типа `Just`, что не соответствует описанию.
**Описание 2:**
Издатель, который объединяет два издателя, сливая их и выдавая в один поток значений, затем применяет преобразование к каждому значению и печатает результаты.
@@ -224,24 +308,19 @@ D. (1...5).publisher.map { Just($0 * $0) }.sink { print($0) }
Варианты:
```swift
A. Publishers.Merge(Just(1), Just(2)).filter { $0 > 1 }.map { $0 * 10 }.sink { print($0) }
[ ] Publishers.Merge(Just(1), Just(2)).filter { $0 > 1 }.map { $0 * 10 }.sink { print($0) }
Пояснение: почти правильный (также объединяет два издателя), но в цепочке преобразований используется `filter`, что не соответствует описанию.
B. Just(1).merge(with: Just(2)).map { $0 * 10 }.sink { print($0) }
[x] Just(1).merge(with: Just(2)).map { $0 * 10 }.sink { print($0) }
Пояснение: Этот фрагмент кода правильно использует метод `merge(with:)` для слияния двух издателей в один поток, применяет преобразование, чтобы умножить каждое выданное значение на 10, и затем печатает преобразованные значения.
C. Just(1).combineLatest(Just(2)).map { $0 * 10 }.sink { print($0) }
[ ] Just(1).combineLatest(Just(2)).map { $0 * 10 }.sink { print($0) }
Пояснение: Так как использован `combineLatest`, который не объединяет издателей в один поток, что не соответствует описанию.
D. [Just(1), Just(2)].publisher.flatMap { $0 }.map { $0 * 10 }.sink { print($0) }
[ ] [Just(1), Just(2)].publisher.flatMap { $0 }.map { $0 * 10 }.sink { print($0) }
Пояснение: Так как `flatMap` не объединяет издателей в один поток, а разворачивает их, что не соответствует описанию.
```
Правильный ответ: B.
Этот фрагмент кода правильно использует метод `merge(with:)` для слияния двух издателей в один поток, применяет преобразование, чтобы умножить каждое выданное значение на 10, и затем печатает преобразованные значения.
Объяснения:
Вариант А — почти правильный (также объединяет два издателя), но в цепочке преобразований используется `filter`, что не соответствует описанию.
Вариант C — ошибочен, так как использован `combineLatest`, который не объединяет издателей в один поток. По той же причине неверен вариант D.
# Подведём итоги
В этом уроке мы вспомнили основные понятия и операции Combine, которые вы изучали в 16-м спринте. В следующем уроке мы выполним практическое задание, чтобы закрепить полученные знания и навыки работы с SwiftUI и Combine.
@@ -1,3 +1,7 @@
# Вёрстка статических экранов с использованием SwiftUI: практическое применение Combine
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В прошлом уроке вы вспомнили основы работы с Combine и проверили знания с помощью квизов. В этом уроке мы будем использовать Combine вместе со SwiftUI, чтобы создать экран Stories.
Вот что нам предстоит:
@@ -12,7 +16,7 @@
## Демоверсия Stories
!(image)[ССЫЛКА НА КАРТИНКУ - Demo Stories Design]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-4_1715771470.png)
Экран нашего приложения будет состоять из следующих элементов:
@@ -32,11 +36,9 @@
Работа над проектом часто начинается с разработки интерфейса пользователя. Это позволяет визуализировать продукт и спланировать архитектуру приложения. В учебном примере мы прежде всего сверстаем статические экраны. Такие экраны не взаимодействуют с пользователем и не обрабатывают динамические данные.
![img](https://github.com/Yandex-Practicum/mobile-iOS/blob/main/lessons-extended-program/20.sprint/02.%20SwiftUI%20%2B%20Combine.%20Практика/pictures_in_color/02_Урок%20статичные%20и%20динамичные.png)
Для начала сверстаем статичный экран Stories, который будет отображать контент, состоящий из эмодзи и текста.
> Мы уже подготовили заготовку проекта — её можно [скачать по ссылке.](ССЫЛКА НА ФАЙЛ - Initial State)
> Мы уже подготовили заготовку проекта — её можно [скачать по ссылке.](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Initial_State.zip)
> Для решения одной и той же задачи в SwiftUI можно использовать разные способы организации кода. Вариант, который предложим мы, не единственно возможный. Однако мы рекомендуем во время выполнения заданий точно следовать шагам из учебника и сверяться с авторскими решениями. Так вы сможете избежать ошибок и более эффективно используете время.
@@ -61,7 +63,7 @@ import SwiftUI
extension Color {
static var story1Background: Color {
Color(red: 232.f/255.f, green: 232.f/255.f, blue: 255.f/255.f)
Color(red: 232.0/255.0, green: 232.0/255.0, blue: 255.0/255.0)
}
}
```
@@ -69,7 +71,7 @@ extension Color {
### Добавляем фоновый цвет для экрана `ContentView`.
!(image)[ССЫЛКА НА КАРТИНКУ - Step 0 - Background]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-1_1715771526.png)
В `var body` укажите возвращаемое значение `Color.Story1Background.ignoresSafeArea()`.
@@ -138,7 +140,7 @@ struct StoryView: View {
Должно получиться вот так:
!(image)[ССЫЛКА НА КАРТИНКУ - Step 1 - Story View]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-3_1715771619.png)
Попробуйте задать нужные настройки в `StoryView` самостоятельно. Сверьтесь с решением ниже.
@@ -232,11 +234,11 @@ struct ContentView: View {
}
```
!(image)[ССЫЛКА НА КАРТИНКУ - Step 2 - Close Button]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399_1715771662.png)
> Верстать анимированный прогресс-бар пока не будем, сделаем это позже.
Экран со статичными данными почти готов. Если нужно, сверьтесь с авторским решением [по ссылке.](ССЫЛКА НА ФАЙЛ - Step 1 - Static Content)
Экран со статичными данными почти готов. Если нужно, сверьтесь с авторским решением [по ссылке.](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_1_-_Static_Content.zip)
## Самостоятельная работа
@@ -266,10 +268,10 @@ struct Story {
Сравните свою работу с образцовым решением, выявите возможные ошибки или упущения и посмотрите, как можно было улучшить собственный проект.
Авторское решение можно найти по [ссылке](ССЫЛКА НА ФАЙЛ - Step 2 - All screens).
Авторское решение можно найти по [ссылке](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_2_-_All_screens.zip).
КНОПКА
Оба экраны готовы!
Оба экрана готовы!
## Добавляем таймер
@@ -310,7 +312,7 @@ struct ContentView: View {
}
```
> Не забудьте, что на предыдущем шаге мы уже добавили массив `stories` для хранения всех историй и переменную `currentStoryIndex` для хранения индекса текущей истории (см. авторское решение выше).
> Обратите внимание, что здесь мы заодно добавили массив `stories` для хранения всех историй и переменную `currentStoryIndex` для хранения индекса текущей истории (см. авторское решение выше).
Теперь можем добавить логику перехода к следующей истории. Для этого создадим метод `nextStory()`, который будет вызываться каждый раз, когда таймер выдаст событие. В методе `nextStory()` мы будем увеличивать индекс текущей истории и обновлять цвет фона и текстовые элементы.
@@ -324,7 +326,7 @@ import SwiftUI
import Combine
struct ContentView: View {
private let stories: [Story] = [ .demo1, .demo2, .demo3 ]
private let stories: [Story] = [ .story1, .story2, .story3 ]
private var currentStory: Story { stories[currentStoryIndex] }
@State private var currentStoryIndex = 0
@State private var timer: Timer.TimerPublisher = Timer.publish(every: 5, on: .main, in: .common)
@@ -362,7 +364,7 @@ struct ContentView: View {
Теперь при каждом срабатывании таймера будет вызываться метод `nextStory()`, который переключает текущую историю на следующую.
!(video)[ССЫЛКА НА ВИДЕО - Step 2 - Timer]
![](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_2_Timer.mp4)
## Добавляем тап по экрану
@@ -373,7 +375,7 @@ import SwiftUI
import Combine
struct ContentView: View {
private let stories: [Story] = [ .demo1, .demo2, .demo3 ]
private let stories: [Story] = [ .story1, .story2, .story3 ]
private var currentStory: Story { stories[currentStoryIndex] }
@State private var currentStoryIndex = 0
@State private var timer: Timer.TimerPublisher = Timer.publish(every: 5, on: .main, in: .common)
@@ -448,7 +450,7 @@ struct ContentView: View {
Теперь при переключении по тапу таймер будет обнуляться, и переключение по таймеру не произойдёт.
!(video)[ССЫЛКА НА ВИДЕО - Step 2 - Timer - Tap]
![](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_2_Timer_Tap.mp4)
# Подведём итоги
@@ -1,3 +1,7 @@
# Анимированный progress bar
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В прошлом уроке мы создали демоверсию приложения, которое имитирует экран Stories: разработали интерфейс пользователя, добавили автоматическое переключение историй по таймеру и тапу.
В этом уроке мы продолжим работу и добавим анимированный прогресс-бар.
@@ -14,17 +18,17 @@
КНОПКА
Приступим
Пойдём попорядку и прежде всего сделаем анимированный прогресс-бар. Создайте новый файл `ProgressBar.swift` и определите в нём структуру `ProgressBar`.
Пойдём по порядку и прежде всего сделаем анимированный прогресс-бар. Создайте новый файл `ProgressBar.swift` и определите в нём структуру `ProgressBar`.
По дизайну, прогресс-бар — это полоска в верхней части экрана, разделённая на `N` секций (где `N` — количество сторис), а текущий прогресс отображается в виде анимированной синей полоски. Чтобы реализовать разделение на секции, мы будем использовать маску.
По дизайну прогресс-бар — это полоска в верхней части экрана, разделённая на `N` секций (где `N` — количество сторис), а текущий прогресс отображается в виде анимированной синей полоски. Чтобы реализовать разделение на секции, мы будем использовать маску.
!(image)[ССЫЛКА НА КАРТИНКУ - Demo Stories Design]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-4_1715771470.png)
Для каждой секции нужно сделать отдельное представление `MaskFragmentView`, которое будет представлять собой одну секцию прогресс-бара. Каждый фрагмент — это прямоугольник с закруглёнными углами и белым цветом. Для создания прямоугольника используем `RoundedRectangle`, а для установки белого цвета — `.foregroundStyle(.white)`. Требуется установить фиксированный размер для фрагмента, чтобы он не растягивался.
Фрагменты маски, сложенные в горизонтальный стек, при условии, что имеется 5 секций, будут выглядеть так:
!(image)[ССЫЛКА НА КАРТИНКУ - MaskFragmentView]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-2_1715771780.png)
### СКРЫВАШКА — НАЧАЛО
Мы используем такой код в `ProgressBar.swift` для превью:
@@ -38,6 +42,8 @@
MaskFragmentView()
MaskFragmentView()
MaskFragmentView()
MaskFragmentView()
MaskFragmentView()
}.padding()
)
}
@@ -67,7 +73,7 @@ struct MaskFragmentView: View {
Маска будет выглядеть приблизительно так:
!(image)[ССЫЛКА НА КАРТИНКУ - MaskView]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-2_1715772050.png)
### СКРЫВАШКА — НАЧАЛО
Мы используем такой код в `ProgressBar.swift` для превью:
@@ -108,7 +114,7 @@ struct MaskView: View {
> Так как нужно будет рассчитывать длину полоски относительно текущего прогресса, то для этого понадобится `GeometryReader`. Внутри него мы создадим `ZStack`, в котором будем отображать прогресс-бар и маску.
Прогресс бар будет представлять собой два прямоугольника: один белый (фон) и один синий (прогресс). Чтобы создать прямоугольники, используем `RoundedRectangle`, а для установки цвета — `.foregroundColor()` (не забудьте добавить цвет из Figma). А чтобы появился эффект разделённости на секции, зададим маску, используя `MaskView`.
Прогресс-бар будет представлять собой два прямоугольника: один белый (фон) и один синий (прогресс). Чтобы создать прямоугольники, используем `RoundedRectangle`, а для установки цвета — `.foregroundColor()` (не забудьте добавить цвет из Figma). А чтобы появился эффект разделённости на секции, зададим маску, используя `MaskView`.
> Если нам не нужно, чтобы `MaskView` или `MaskFragmentView` использовались где-то ещё, то помечаем их как `private`.
@@ -194,7 +200,7 @@ private struct MaskFragmentView: View {
Прогресс-бар, при условии, что секций 5, будет выглядеть так:
!(image)[ССЫЛКА НА КАРТИНКУ - ProgressBar]
![](https://pictures.s3.yandex.net/resources/PRODPP-8399-5_1715772092.png)
Добавьте `ProgressBar` в `ContentView.swift`.
@@ -261,7 +267,7 @@ struct ContentView: View {
Теперь при переключении историй прогресс-бар будет обновляться.
!(video)[ССЫЛКА НА ВИДЕО - Discrete progress bar]
![](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Discrete_progress_bar.mp4)
## Плавное заполнение прогресс-бара
@@ -273,7 +279,7 @@ struct ContentView: View {
Вот что должно получиться:
!(video)[ССЫЛКА НА ВИДЕО - Animated progress bar]
![](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_3_Add_progress_bar.mp4)
Попробуйте сделать самостоятельно.
@@ -302,7 +308,7 @@ struct ContentView: View {
init(
storiesCount: Int,
secondsPerStory: TimeInterval = 5,
timerTickInternal: TimeInterval = 0.25
timerTickInternal: TimeInterval = 0.05
) {
self.timerTickInternal = timerTickInternal
self.progressPerTick = 1.0 / CGFloat(storiesCount) / secondsPerStory * timerTickInternal
@@ -373,7 +379,7 @@ struct ContentView: View {
}
```
Финальное решение можно посмотреть по [ссылке](ССЫЛКА НА ФАЙЛ - Step 3 - Add progress bar).
Финальное решение можно посмотреть по [ссылке](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_3_Add_progress_bar.zip).
## Анимация при переключении историй
@@ -381,14 +387,14 @@ struct ContentView: View {
Должно получиться примерно так:
!(video)[ССЫЛКА НА ВИДЕО - Step 4 - Animated progress bar - withAnimation]
![](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_4_Animated_progress_bar_withAnimation.mp4)
Попробуйте сделать самостоятельно. Сверьтесь с решением ниже.
КНОПКА
Посмотреть решение
Авторское решение можно посмотреть по [ссылке](ССЫЛКА НА ФАЙЛ - Step 4 - Animated progress bar - withAnimation).
Авторское решение можно посмотреть по [ссылке](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_4_Add_animations.zip).
## Задача повышенной сложности
@@ -410,7 +416,7 @@ struct ContentView: View {
Рекомендуется также упорядочить файлы и сгруппировать их по категориям (Views, DataObjects, Algorithms, Helpers, Assets).
Для самопороверки используйте авторское решение. Его можно скачать по [ссылке](ССЫЛКА НА ФАЙЛ - Step 5 - Final).
Для самопроверки используйте авторское решение. Его можно скачать по [ссылке](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_20/theme_2/Step_5_Final.zip).
# Подведём итоги
@@ -1,35 +1,47 @@
Вы завершили 20 спринт, в котором изучили анимации, использовали жесты, сложные анимации и попрактиковались в использовании SwiftUI + Combine при реализации упрощ`нного экрана Stories. Пора приступать к заданию для самостоятельной работы!
# Сдаём задачу спринта 20 на ревью
Вы завершили 20 спринт, в котором изучили анимации, использовали жесты, сложные анимации и попрактиковались в использовании SwiftUI + Combine при реализации упрощённого экрана Stories. Пора приступать к заданию для самостоятельной работы!
Вам предстоит сверстать экраны по [макету](https://www.figma.com/design/cQ97drl0RAec3i7vNh55Ta/Расписание). Так вы закрепите на практике теорию и навыки, которые получили на курсе.
## Задание
В проекте расписание путешествий нужно:
1. В проекте "Расписание путешествий" нужно завершить вёрстку по макету:
- на главном экране добавить горизонтальную коллекцию Stories:
- Добавить экран с карточкой перевозчика
![](./images/1_main_screen_stories_collection.png)
![](./images/info.png)
- реализовать, чтобы при тапе на иконку в панели Stories отображался экран Stories:
- Добавить экран настроек (с возможностью переключения в тёмную тему и пользовательским соглашением);
![](./images/settings.png)
- На главном экране добавить горизонтальную коллекцию Stories:
![](https://pictures.s3.yandex.net/resources/PRODPP-8517-1_1715772191.png)
2. Реализовать, чтобы при тапе на иконку в панели Stories отображался экран Stories:
![](https://pictures.s3.yandex.net/resources/PRODPP-8517_1715772527.png)
3. Реализовать переключение на тёмную тему в зависимости от переключателя на экране настроек.
![](./images/2_stories.png)
Для наполнения коллекции Stories используйте статические данные — макеты из Figma.
## Чек-лист
- Пользовательское соглашение показывается полноэкранно, то есть перекрывает TabBar при показе.
- На главном экране в верхней части отображается _коллекция_ Stories;
![](images/4_stories_collection.png)
![](https://pictures.s3.yandex.net/resources/PRODPP-8517-2_1715772761.png)
- Для коллекции Stories верно:
- просмотренные и не просмотренные истории отличаются друг от друга визуально: не просмотренные истории имеют синюю обводку и яркую картинку, просмотренные — картинку с изменённой прозрачностью;
**(КОММЕНТАРИЙ ДЛЯ РЕДАКТОРА/ИЛЛЮСТРАТОРА: МОЖНО НА ОДНУ ПОДЛОЖКУ ПОЛОЖИТЬ ОБА СОСТОЯНИЯ)**
![](./images/3_1_stories-cell-unread.png)
![](./images/3_2_stories-cell-read.png)
![](https://pictures.s3.yandex.net/resources/PRODPP-8517-3_1715772898.png)
- коллекцию можно пролистывать влево и вправо;
@@ -43,21 +55,27 @@
- нажатием на крестик в верхнем правом углу можно закрыть экран Stories.
- Дизайн проекта полностью соответствует дизайну макета в Figma, включая все элементы, шрифты, цвета и отступы.
- Тёмная/светлая тема меняются в зависимости от переключателя на экране настроек и не меняются при смене пользователем тёмной/светлой темы в системе.
> Прежде чем сдавать проект на ревью, пройдитесь по каждому пункту чек-листа. Полное описание функциональных требований лежит в [техническом задании](https://github.com/Yandex-Practicum/travel_schedule){target="\_blank"}. Если ваша работа не проходит по какому-то из пунктов — доделайте её до сдачи.
> Не забывайте обращаться к наставникам, если возникнут вопросы.
## Порядок сдачи работы
1. Выполните задачу в Xcode.
2. После проверки по чек-листу загрузите готовую задачу в ваш проект в GitHub в отдельную ветку с названием **sprint_20**. Проект должен быть **открытым**.
3. Создайте Pull Request и скопируйте ссылку на него. Вставьте её в специальную форму на сайте Практикума через кнопку «Сдать работу».
1. Убедитесь, что пулл-реквесты предыдущих спринтов смержены в main.
2. Выполните задачу в Xcode.
3. После проверки по чек-листу загрузите готовую задачу в ваш проект в GitHub в отдельную ветку с названием **sprint_20**. Проект должен быть **открытым**.
4. Создайте Pull Request и скопируйте ссылку на него. Вставьте её в специальную форму на сайте Практикума через кнопку «Сдать работу».
## Задачи со звёздочкой
Если после выполнения обязательных требований вы хотите получить больше практического опыта с SwiftUI, вы можете:
1. Добавить нестандартные анимации переходов между экранами. Например, сделать так, чтобы на экран Stories при открытии анимированно увеличивались иконки и текст.
2. Добавьте возможность закрытия экрана Stories свайпом вниз.
2. Добавить возможность закрытия экрана Stories свайпом вниз.
3. Для отображения пользовательского соглашения использовать не TextView, а WebView.
Удачи! Всё получится!
@@ -17,5 +17,3 @@
- реализуете экран с использованием MVVM, SwiftUI и Combine;
- свяжете сетевой слой и графический интерфейс.
Желаем успехов!
@@ -72,7 +72,7 @@ MVVM (Model-View-ViewModel) — это шаблон проектирования
## Почему MVVM отлично подходит для SwiftUI
Ранее вы изучали, как организовать MVVM с использованием замыканий — так мы реализовывали уведомления об изменениях состояния ViewModel. Фреймворк Combine подходит для этой задачи гораздо лучше: он обеспечивает более чистую и управляемую реактивную архитектуру, позволяя легче справляться с асинхронными операциями и изменениями состояния данных.
Ранее вы изучали, как организовать MVVM с использованием замыканий — так мы реализовывали уведомления об изменениях состояния ViewModel. Фреймворк Combine подходит для этой задачи гораздо лучше: он обеспечивает более чистую и управляемую реактивную архитектуру, позволяя легче справляться с асинхронными операциями и изменениями состояния данных.
MVVM отлично подходит для разработки приложений на SwiftUI благодаря своей способности эффективно интегрироваться с декларативным подходом фреймворков SwiftUI и Combine. В MVVM ViewModel предоставляет данные и команды в удобной для отображения форме, что позволяет использовать связывание данных для автоматического обновления интерфейса при изменении данных. ViewModel не зависит от View, что и поддерживает динамичное обновление интерфейса через состояние.
@@ -1,5 +1,7 @@
# Реализация MVVM на SwiftUI с использованием Combine
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В этом уроке мы рассмотрим, как можно использовать паттерн MVVM (Model-View-ViewModel) в сочетании с фреймворком Combine для разработки приложений на SwiftUI.
> Паттерн MVVM помогает организовать код так, чтобы он был более модульным и облегчал тестирование, а Combine упрощает работу с асинхронными операциями и управлением состояния.
@@ -21,21 +23,67 @@
* **В архитектуре MVP** Presenter напрямую управляет View, обновляя его и реагируя на действия пользователя с помощью вызовов методов, что делает View пассивным.
* **В архитектуре MVVM** ViewModel не знает о View, с которым она взаимодействует, а изменения в данных автоматически отражаются в View через механизмы привязки данных (data binding) — эти механизмы ещё называют наблюдателями за свойствами. Это позволяет View быть более независимой и упрощает тестирование ViewModel, так как она не зависит от конкретных реализаций пользовательского интерфейса. Это делает MVVM особенно подходящий для фреймворка SwiftUI с его декларативным стилем описания интерфейса.
## Ещё раз о Combine
## Инструменты связывания данных (data binding)
**Combine** — это фреймворк для обработки асинхронных событий путём объединения потоков данных. Он использует концепции реактивного программирования и позволяет элегантно реагировать на изменения в данных, что идеально подходит для использования с MVVM.
В архитектуре MVVM связывание данных организуется таким образом, что ViewModel содержит данные и логику состояния, которые автоматически синхронизируются с пользовательским интерфейсом через механизмы привязки данных. В настоящее время таких механизмов существует два: один использовался до iOS 17, другой - начиная с iOS 17 и выше.
Хотя фреймворк Combine достаточно сложный, для создания экрана на архитектурном паттерне MVVM мы будем использовать лишь несколько ключевых слов, с которыми вы уже сталкивались ранее:
> Несмотря на то, что iOS 17 вышла уже достаточно давно, не стоит ожидать, что во всех проектах ваших будущих заказчиков и работодателей будет использоваться именно новый механизм. Рефакторинг - задача большая и трудоёмкая; кроме того, многие хотят, чтобы их приложения поддерживали обратную совместимость на несколько версий iOS назад. Вот почему мы с вами разберём здесь оба варианта - и с `ObservableObject`, и с '@Observable`.
Оба механизма реализуют одну и ту же идею: они позволяют нам отметить объект как «наблюдаемый» - это значит, что такой объект будет уведомлять о происходящих в нём изменениях все представления, где они используются. Такое уведомление автоматически инициирует перерисовку соответствующих компонентов интерфейса. Это обеспечивает реактивное обновление View без необходимости ручного управления и обновления, что значительно упрощает разработку и поддержку кода.
## Создание архитектуры MVVM в SwiftUI: пошаговое руководство
Сначала рассмотрим старый вариант. Со всеми его компонентами вы уже сталкивались ранее:
- `ObservableObject` — это протокол в SwiftUI, который позволяет объектам класса быть «наблюдаемыми». Классы, реализующие этот протокол, могут уведомлять свои представления о том, что некоторые из их данных были изменены. Классы, реализующие `ObservableObject`, часто используются для создания моделей представления в архитектуре MVVM.
- `@ObservedObject` — это проперти враппер в SwiftUI, который используется для создания связи между представлением и классом `ObservableObject`. При использовании `@ObservedObject`, SwiftUI следит за изменениями в `ObservableObject` и обновляет представление при изменении данных. Этот враппер подходит для случаев, когда объект данных создаётся вне представления и передаётся в него, например, через параметры конструктора.
- `@Published` — это проперти враппер, который автоматически добавляет функционал уведомления об изменениях к переменной в классе, реализующем ObservableObject. Каждый раз, когда значение переменной, отмеченной как `@Published`, изменяется, ObservableObject отправляет уведомление всем наблюдателям.
- `@ObservedObject` — это property wrapper в SwiftUI, который используется для создания связи между представлением и классом `ObservableObject`. При использовании `@ObservedObject`, SwiftUI следит за изменениями в `ObservableObject` и обновляет представление при изменении данных. Этот враппер подходит для случаев, когда объект данных создаётся вне представления и передаётся в него, например, через параметры конструктора.
- `@Published` — это property wrapper, который автоматически добавляет функционал уведомления об изменениях к переменной в классе, реализующем ObservableObject`. Каждый раз, когда значение переменной, отмеченной как `@Published`, изменяется, ObservableObject` отправляет уведомление всем наблюдателям.
### Шаг 1: создание ViewModel и объявление её как ObservableObject
Для начала создайте класс ViewModel, который будет управлять данными и логикой вашего экрана. Важно, что ViewModel должна быть именно классом, так как структуры в Swift не поддерживают наследование и не могут использоваться с `ObservableObject`.
```swift
class MyViewModel: ObservableObject {
// ...
}
```
### Шаг 2: отметить поля для наблюдения с помощью @Published
В вашем классе ViewModel каждое свойство, изменения которого вы хотите отслеживать и которые должны вызывать обновление интерфейса, необходимо пометить атрибутом `@Published`.
```swift
class MyViewModel: ObservableObject {
@Published var someData: String = ""
@Published var anotherValue: Int = 0
// Добавьте здесь необходимые свойства и методы
}
```
### Шаг 3: Использование ViewModel во View
Чтобы использовать данные и логику ViewModel в вашем View, необходимо объявить экземпляр ViewModel как `@ObservedObject`. Это установит связь между вашей ViewModel и View, позволяя View реагировать на изменения в ViewModel.
```swift
struct MyView: View {
@ObservedObject var viewModel = MyViewModel()
var body: some View {
Text("Значение: \(viewModel.someData)")
// Дополнительные элементы интерфейса, которые используют данные из ViewModel
}
}
```
Эти шаги создадут основу для использования архитектуры MVVM в вашем проекте на SwiftUI.
## Пример создания экрана на основе архитектурного паттерна MVVM
Создадим простой экран со списком задач, используя SwiftUI, MVVM и Combine.
> Вы можете скачать [подготовленный проект](SimpleTaskTrackerMVVMDemo.zip) для изучения.
> Вы можете скачать [подготовленный проект](https://code.s3.yandex.net/Mobile/iOS/Playground_lessons/sprint_21/theme_1/SimpleTaskTrackerMVVMDemo.zip) для изучения.
![image](/21.sprint/01.theme%20MVVM+Combine/images/mvvm-demo-project-1.png)
@@ -74,83 +122,38 @@ class TasksViewModel: ObservableObject {
### Шаг 3: Создаём View
```swift
struct TasksView: View {
struct ContentView: View {
@ObservedObject var viewModel: TasksViewModel
var body: some View {
List {
ForEach(viewModel.tasks) { task in
HStack {
Text(task.title)
.strikethrough(task.completed)
Spacer()
Button(action: {
viewModel.toggleComplete(task: task)
}) {
Image(systemName: task.completed
? "checkmark.circle.fill"
: "circle"
)
NavigationView {
List {
ForEach(viewModel.tasks) { task in
HStack {
Text(task.title)
.strikethrough(task.completed)
Spacer()
Button(action: {
viewModel.toggleComplete(task: task)
}) {
Image(systemName: task.completed
? "checkmark.circle.fill"
: "circle"
)
}
}
}
}
.navigationBarItems(trailing: Button(action: {
viewModel.addTask(title: "Новая задача")
}) {
Text("Добавить")
})
}
.navigationBarItems(trailing: Button(action: {
viewModel.addTask(title: "Новая задача")
}) {
Text("Добавить")
})
}
}
```
## Инструменты связывания данных (data binding)
В архитектуре MVVM связывание данных организуется таким образом, что ViewModel содержит данные и логику состояния, которые автоматически синхронизируются с пользовательским интерфейсом через механизмы привязки данных. Это достигается за счёт использования проперти врапперов `@Published`, `ObservableObject`, `@ObservedObject`.
> ViewModel реализуется как `ObservableObject`, что позволяет отслеживать изменения в его свойствах. Компоненты интерфейса, в свою очередь, могут использовать `@ObservedObject` для подписки на эти изменения. Когда свойства, отмеченные как `@Published`, обновляются, SwiftUI автоматически инициирует перерисовку соответствующих компонентов интерфейса. Это обеспечивает реактивное обновление View без необходимости ручного управления и обновления, что значительно упрощает разработку и поддержку кода.
# Создание архитектуры MVVM в SwiftUI: пошаговое руководство
## Шаг 1: создание ViewModel и объявление её как ObservableObject
Для начала создайте класс ViewModel, который будет управлять данными и логикой вашего экрана. Важно, что ViewModel должна быть именно классом, так как структуры в Swift не поддерживают наследование и не могут использоваться с `ObservableObject`.
```swift
class MyViewModel: ObservableObject {
// ...
}
```
## Шаг 2: отметить поля для наблюдения с помощью @Published
В вашем классе ViewModel каждое свойство, изменения которого вы хотите отслеживать и которые должны вызывать обновление интерфейса, необходимо пометить атрибутом `@Published`.
```swift
class MyViewModel: ObservableObject {
@Published var someData: String = ""
@Published var anotherValue: Int = 0
// Добавьте здесь необходимые свойства и методы
}
```
## Шаг 3: Использование ViewModel во View
Чтобы использовать данные и логику ViewModel в вашем View, необходимо объявить экземпляр ViewModel как `@ObservedObject`. Это установит связь между вашей ViewModel и View, позволяя View реагировать на изменения в ViewModel.
```swift
struct MyView: View {
@ObservedObject var viewModel = MyViewModel()
var body: some View {
Text("Значение: \(viewModel.someData)")
// Дополнительные элементы интерфейса, которые используют данные из ViewModel
}
}
```
Эти шаги создадут основу для использования архитектуры MVVM в вашем проекте на SwiftUI.
## Применение макроса `@Observable`
Макрос `@Observable`, представленный в iOS 17, является усовершенствованием, позволяющим упростить синтаксис при объявлении ViewModel. Этот макрос заменяет использование `ObservableObject` и связанных с ним аннотаций `@Published`, обеспечивая более чистый код и эффективную реализацию.
@@ -223,6 +226,8 @@ struct CounterView: View {
`@State` в сочетании с `@Observable` в SwiftUI используют для обеспечения правильного управления жизненным циклом состояния в представлениях, а именно, это помогает сохранять состояние данных в представлении при его пересоздании. Этот подход позволяет избежать потери данных и неожиданного поведения приложения при обновлениях интерфейса или изменении ViewModel из другой части приложения.
Для лучшего понимания, как соотносятся старая и новая версии, рекомендуем ознакомиться [с руководством по миграции с `ObservableObject` на `@Observable`]. (https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro)
## Подведём итоги
Использование MVVM с Combine в SwiftUI может значительно упростить управление состоянием и асинхронными операциями в вашем приложении. Начните с простых примеров, как показано выше, и постепенно добавляйте больше функциональности, чтобы лучше понять, как эти инструменты могут работать вместе. Не бойтесь экспериментировать!
@@ -6,17 +6,16 @@
## Задание
Выберите любой экран приложения «Расписание Путешествий» и переведите его на архитектуру MVVM. Например, экран настроек как наиболее простой, или первый экран с полями «откуда» и «куда» — он более комплексный.
Выберите любой экран приложения «Расписание Путешествий» и переведите его на архитектуру MVVM. Например, экран настроек как наиболее простой или первый экран с полями «откуда» и «куда» — он более комплексный.
## Шаги реализации
1. **Создайте ViewModel:**
- Создайте новый файл для ViewModel: например, `SettingsViewModel.swift` или `TravelViewModel.swift`.
- Объявите класс ViewModel и пометьте его как `ObservableObject`.
- Объявите класс ViewModel и пометьте его как `Observable`.
2. **Добавьте необходимые свойства:**
- Определите, какие данные нужно хранить во `ViewModel`. Например, для экрана настроек это могут быть пользовательские предпочтения выбранной темы оформления, а для экрана путешествий — данные о выбранных местах отправления и назначения.
- Пометьте изменяемые данные как `@Published`, чтобы они могли активировать обновления UI.
3. **Создайте методы для получения и обработки данных:**
- Если данные приходят из сети, пользовательских настроек UserDefaults или базы данных, создайте соответствующие методы для их загрузки.
@@ -24,7 +23,7 @@
4. **Интегрируйте ViewModel с `View`:**
- Добавьте в представление (`View`) `ViewModel`.
- Используйте `@ObservedObject` для связи представления и вью модели.
- Используйте `@State` для связи представления и вью модели.
5. **Настройте биндинги (связи) и обновления `View`:**
- Обеспечьте обновление `View` при изменении данных в `ViewModel`.
@@ -38,9 +37,8 @@
**Убедитесь, что:**
- `ViewModel` помечена как `@ObservableObject`.
- Все изменяемые данные во `ViewModel` помечены как `@Published`, если от их изменения зависит UI.
- `View` использует `@ObservedObject` для интеграции с `ViewModel`.
- `ViewModel` помечена как `@Observable`.
- `View` использует `@State` для интеграции с `ViewModel`.
- `ViewModel` не содержит ссылок на `View`, только данные и логику.
- Код соответствует принципам SOLID и MVVM — разделение обязанностей ясно выражено.
@@ -10,48 +10,49 @@
Несмотря на большое число инструментов и возможностей для работы в многопоточной среде, авторы Swift пошли дальше и добавили поддержку структурной многопоточности. Но что это такое?
Прежде чем мы перейти к новым инструментам работы, давайте вспомним уже изученное. Так будет проще понять различия и преимущества нового подхода.
Прежде чем перейти к новым инструментам работы, давайте вспомним уже изученное. Так будет проще понять различия и преимущества нового подхода.
**Кнопка**
👌🏻
# Повторим изученное
## Повторим изученное
Итак, начнём с очередей. Какие виды очередей в Swift вы помните?
**КВИЗ**
**КВИЗ - НАЧАЛО**
- [x] Serial
**Верно! Задачи на serial-очереди выполняются по очереди.**
Пояснение: Верно! Задачи на serial-очереди выполняются по очереди.
- [ ] Sync
**Пояснение: Sync относится к типу добавляемых на очередь задач, а не к типу очереди.**
Пояснение: Sync относится к типу добавляемых на очередь задач, а не к типу очереди.
- [ ] Async
**Пояснение: Async относится к типу добавляемых на очередь задач, а не к типу очереди.**
Пояснение: Async относится к типу добавляемых на очередь задач, а не к типу очереди.
- [x] Concurrent
**Верно! Задачи на concurrent очереди могут выполняться параллельно.**
Пояснение: Верно! Задачи на concurrent очереди могут выполняться параллельно.
Хорошо, а какой тип у очереди main?
**КВИЗ**
- [x] Serial
**Пояснение: Верно!
Пояснение: Верно!
- [ ] Sync
**Пояснение: Sync относится к типу добавляемых на очередь задач, а не к типу очереди.**
Пояснение: Sync относится к типу добавляемых на очередь задач, а не к типу очереди.
- [ ] Async
**Пояснение: Async относится к типу добавляемых на очередь задач, а не к типу очереди.**
Пояснение: Async относится к типу добавляемых на очередь задач, а не к типу очереди.
- [ ] Concurrent
**Пояснение: Main-очередь отвечает за задачи на главном потоке, где важен порядок выполнения задач, поэтому главная очередь — serial.**
Пояснение: Main-очередь отвечает за задачи на главном потоке, где важен порядок выполнения задач, поэтому главная очередь — serial.
А чего нельзя делать на главной очереди?
**КВИЗ**
- [ ] Отправлять асинхронные запросы напрямую в хранилище
**Пояснение: Не совсем: это хоть и крайне нежелательно, и лучше разделять приложения на слои, но технически асинхронно отправлять запросы с главной очереди можно.**
Пояснение: Не совсем: это хоть и крайне нежелательно, и лучше разделять приложения на слои, но технически асинхронно отправлять запросы с главной очереди можно.
- [ ] Добавить сразу несколько асинхронных задач на изменение фона кнопки
**Пояснение: За счёт того, что главная очередь является serial, задачи будут выполнены в порядке добавления, и кнопка применит последний заданный фон.**
Пояснение: За счёт того, что главная очередь является serial, задачи будут выполнены в порядке добавления, и кнопка применит последний заданный фон.
- [x] Добавить сразу несколько синхронных задач на изменение фона кнопки
**Верно! Не важно, что вы делаете в своих задачах — если на главную очередь, как и на любую другую serial-очередь, добавить синхронную задачу, то приложение упадёт.**
Пояснение: Верно! Не важно, что вы делаете в своих задачах — если на главную очередь, как и на любую другую serial-очередь, добавить синхронную задачу, то приложение упадёт.
- [ ] Вызвать внутри асинхронной задачи main.async { }
**Пояснение: Такой код не вызовет проблем во время исполнения.**
Пояснение: Такой код не вызовет проблем во время исполнения.
Как называется проблема, когда вы пытаетесь добавить на serial-очередь синхронную задачу?
@@ -60,14 +61,14 @@
- [x] deadlock
- [ ] race condition
- [ ] retain cycle
**Пояснение: Retain cycle относится к проблемам при работе с памятью.**
Пояснение: Пояснение: Retain cycle относится к проблемам при работе с памятью.
**Кнопка**
С терминами разобрались!
Давайте посмотрим на уже привычный вам код:
```
```swift
func requestSomeStrings(completion: @escaping ((Result<[String], Error>) -> Void)) {
let request = buildRequest()
@@ -101,7 +102,7 @@ func requestSomeStrings(completion: @escaping ((Result<[String], Error>) -> Void
Корректный код должен вызывать `completion` в случае, когда ответ пустой (`nil`), и возвращать ошибку. Для этого необходимо создать перечисление с описанием ошибки, а также добавить внутрь `guard` вызов замыкания до того, как будет выполнен `return`.
```
```swift
enum ResponseError: Error {
case noResponse
}
@@ -132,8 +133,6 @@ func requestSomeStrings(completion: @escaping ((Result<[String], Error>) -> Void
> Проблема ошибок в многопоточности в том, что они крайне «дорогие» в отладке.
КАРТИНКА https://github.com/Yandex-Practicum/mobile-iOS/pull/500/files#r1606973258
Другая проблема многопоточности в том, что в коде зачастую получается большая вложенность, а это тоже усложняет отладку. Представьте, что вам нужно вернуть не массив строк, а массив данных, который нужно получить для каждой из строк. Конечно, можно разделить код на отдельные функции, сделать универсальные методы для загрузки, но от излишней вложенности всё равно будет очень сложно избавиться. В лучшем случае вы лишь сделаете код более читаемым.
> Это связано с тем, что в текущей многопоточности нарушается принцип структурного программирования.
@@ -148,7 +147,7 @@ func requestSomeStrings(completion: @escaping ((Result<[String], Error>) -> Void
Студент: Но что происходит в случае многопоточности?
Практикум: Мы не можем остановить выполнение кода, пока ожидаем ответ от сети, нужно идти дальше!
Приложение должно оставаться активным, а пользователь должен иметь возможность взаимодействовать с кодом. Когда будет получен результат запроса, мы выполним блок обработки вне зависимости от того, как далеко уже ушли по коду. В случае работы с многопоточностью, мы не можем и вернуть результат выполнения с помощью `return` и должны использовать замыкания.
Приложение должно оставаться активным, а пользователь должен иметь возможность взаимодействовать с кодом. Когда будет получен результат запроса, мы выполним блок обработки вне зависимости от того, как далеко уже ушли по коду. В случае работы с многопоточностью мы не можем и вернуть результат выполнения с помощью `return` и должны использовать замыкания.
КНОПКА-ДИАЛОГ
Студент: Нужно забыть старый подход и учить новый?
@@ -158,10 +157,10 @@ func requestSomeStrings(completion: @escaping ((Result<[String], Error>) -> Void
> На заре iOS-разработки для создания приложений использовали `Objective-C`, с его помощью разработчики успешно создавали свои продукты. Но спустя годы появился `Swift` и постепенно вытеснил `Objective-C`. Это был небыстрый процесс: поначалу в старых продуктах использовался `Swift` для написания новых блоков, дальше потихоньку переписывались части приложения с `Objective-C` на `Swift`.
Тоже касается и многопоточности: важно знать и уметь работать с обоими подходами, ведь серьёзных проектов, использующих только новый подход не так уж и много. Впрочем, полагаться только на старый подход тоже не стоит — структурная многопоточность набирает популярность и всё чаще используется в новых проектах, либо интегрируется в старые.
То же самое касается и многопоточности: важно знать и уметь работать с обоими подходами, ведь серьёзных проектов, использующих только новый подход не так уж и много. Впрочем, полагаться только на старый подход тоже не стоит — структурная многопоточность набирает популярность и всё чаще используется в новых проектах, либо интегрируется в старые.
# Подведём итоги
## Подведём итоги
Блок уроков, которые вам предстоит пройти, нужен, чтобы вы были на одной волне с новыми технологиями. Даже если в компании используют GCD, про новую технологию довольно часто могут спросить на собеседовании. Владение этими знаниями показывает насколько кандидат интересуется новшествами, открыт к развитию. А, возможно, компания готовится к переходу, и им важно найти человека, который уже знаком с новым подходом к многопоточности.
@@ -1,12 +1,16 @@
# Структурированная многопоточность
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В предыдущем уроке вы познакомились со структурным программированием и взглянули на проблемы текущего подхода. Теперь мы изучим новый подход к многопоточности в `Swift` — структурную многопоточность.
> Swift Concurrency — так официально называется новая технология — доступна начиная с iOS 15+. Если вам нужна поддержка более ранних версий iOS, то придётся подождать с переходом.
> Swift Concurrency — так официально называется новая технология — доступна начиная с iOS 15+. В версиях iOS 17 и 18 эта технология стала стабильнее и мощнее, был значительно улучшен анализ потенциальных проблем многопоточности.
# async/await
## async/await
Как выглядит обычная функция в Swift: название, параметры, возвращаемый тип. С помощью нового подхода появляется возможность писать многопоточный код точно так же: многопоточные функции будут возвращать значения с помощью `return`, когда закончат задачу, а в случае возникновения ошибки будут возвращать ошибку.
КАРТИНКА С СИНТАКСИСОМ
![](https://pictures.s3.yandex.net/resources/image_1718634739.png)
Давайте перепишем пример из прошлого урока с помощью нового синтаксиса:
@@ -41,7 +45,7 @@ func testPrints() async {
}
func intWithDelay() async throws -> Int {
try await Task.sleep(nanoseconds: 50)
try await Task.sleep(for: .milliseconds(50))
return Int.random(in: 0..<Int.max)
}
```
@@ -55,7 +59,7 @@ func intWithDelay() async throws -> Int {
С помощью ключевого слова `await` необходимо помечать вызов async функций, если же вызываемая функция не помечена как асинхронная, то это обычная синхронная функция.
При этом вы можете пометить и синхронную функцию ключевым словом async и при её вызове будет необходимо указывать await. Несмотря на то что чисто технически это возможно, это не имеет никакого практического смысла и не стоит так делать.
При этом вы можете пометить и синхронную функцию ключевым словом `async` и при её вызове будет необходимо указывать `await`. Несмотря на то, что чисто технически это возможно, это не имеет никакого практического смысла и не стоит так делать.
**КВИЗ**
@@ -70,7 +74,7 @@ func testPrints() async {
}
func intWithDelay() async throws -> Int {
try await Task.sleep(nanoseconds: 50)
try await Task.sleep(for: .milliseconds(50))
return 7
}
```
@@ -112,7 +116,7 @@ func intWithDelay() async throws -> Int {
**Пояснение: Хоть функция `intWithDelay` по факту является синхронной, она помечена, как `async` и вызов такой функции должен быть помечен ключевым словом `await`.
# Несколько async-вызовов
## Несколько async-вызовов
Часто может возникать ситуация, когда вам нужно в рамках одной функции выполнить сразу несколько независимых асинхронных задач, а затем произвести действие с результатом. Например, вы загружаете альбом исполнителя и помимо списка композиций хотите скачать файл с обложкой. Эти две задачи не зависят друг от друга, и для экономии времени можно выполнять их параллельно. Давайте посмотрим, что предлагает `Swift` для реализации этого.
@@ -133,7 +137,7 @@ func multipliAwaitInts() async -> Int {
}
func intWithDelay() async -> Int {
try? await Task.sleep(nanoseconds: 50)
try? await Task.sleep(for: .milliseconds(50))
return 7
}
@@ -148,7 +152,7 @@ await multipliAwaitInts()
Для таких ситуаций в Swift есть решение — `async let`.
КАРТИНКА С СИНТАКСИСОМ
![](https://pictures.s3.yandex.net/resources/340332009-2b47fda6-4e89-42f6-b7cb-ff4f78683bec_1718634894.png)
Конструкция `async let` позволяет присвоить переменной значение выполнения асинхронной функции, не останавливая выполнения кода до момента, когда значение переменной становится необходимым. Код, где необходимо остановиться для ожидания значения, необходимо помечать с помощью `await`.
@@ -165,7 +169,7 @@ func multipliAwaitInts() async -> Int {
func intWithDelay() async -> Int {
print("start")
try? await Task.sleep(nanoseconds: 50)
try? await Task.sleep(for: .milliseconds(50))
print("ready")
return 7
}
@@ -182,7 +186,7 @@ func multiply(val1: Int, val2: Int) {
func intWithDelay() async -> Int {
print("start")
try? await Task.sleep(nanoseconds: 50)
try? await Task.sleep(for: .milliseconds(50))
print("ready")
return 7
}
@@ -1,10 +1,15 @@
# Интеграция в существующие проекты
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
Проблема почти любой новой технологии — ей требуется время, чтобы получить популярность. Так случилось и в `Swift`. Новый подход был представлен несколько лет назад, и много библиотек не было адаптировано. А в крупных проектах переписать весь многопоточный код — это непростая задача, чаще стараются постепенно адаптироваться. В этом уроке мы разберём, как можно совмещать сразу два подхода.
![img](https://github.com/Yandex-Practicum/mobile-iOS/blob/20sprint/concurrency/lessons-extended-program/20.sprint/2.%20Многопоточность/pictures%20in%20color/урок02_перерыв.png)
# Связь старого и нового подходов
## Связь старого и нового подходов
Итак, возьмём за пример функцию `fetchStrings`, использующую уже хорошо знакомые нам замыкания (`completionHandler`). Предположим, что у нас нет возможности переписать эту функцию, так как она — часть библиотеки.
```
func fetchStrings(from url: URL, completion: @escaping ([String]) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
@@ -58,7 +63,7 @@ func fetchMessages() async {
}
```
# Небезопасная связь кода
## Небезопасная связь кода
Как было описано выше, в случае, если вы забудете вызвать resume, в консоль будет выведено сообщение об ошибке. Это достигается за счёт того, что в случае использования `CheckedContinuation` во время выполнения кода происходит проверка корректности использования continuation. Если же вы хотите немного оптимизировать код и абсолютно точно уверены, что в исходной функции с замыканиями нет проблем, то можно избежать проверки во время выполнения за счёт использования `UnsafeContinuation`.
@@ -1,3 +1,5 @@
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
Итак, осталось не так много из основ нового подхода к многопоточности. В этом уроке вы узнаете, что такое `Task`, как объединять сразу несколько задач и как работать с массивом данных в многопоточной среде.
# Task
@@ -6,22 +8,22 @@ Task (задача), часто на русском языке может пис
Но давайте по порядку!
КАРТИНКА С СИНТАКСИСОМ (struct Task<Success, Failure> where Success : Sendable, Failure : Error)
![](https://pictures.s3.yandex.net/resources/340332787-71ab7e57-9a9f-450c-93fb-541922a939c8_1719226451.png)
Таски — это структуры (значит, передаются по значению, а не по ссылке). `Task` создаётся, когда вам нужно выполнить фоново какую-то задачу, не останавливая выполнение программы. Например, внутри UI-кода.
Таски — это структуры (значит, передаются по значению, а не по ссылке). `Task` создаётся, когда вам нужно выполнить асинхронно какую-то задачу, не останавливая выполнение программы. Например, внутри UI-кода.
```
```swift
override func viewDidLoad() {
super.viewDidLoad()
super.viewDidLoad()
Task {
do {
let image = try await networkManager.loadImage(id: imageID)
didLoadImage(image)
} catch {
didFailLoadImage(error)
}
Task {
do {
let image = try await networkManager.loadImage(id: imageID)
didLoadImage(image)
} catch {
didFailLoadImage(error)
}
}
}
private func didLoadImage(_ image: UIImage) {
@@ -44,9 +46,9 @@ private func didFailLoadImage(_ error: Error) {
Если вы работаете со `SwiftUI`, то там свойство `body` у протокола `View` также помечено `@MainActor`.
Почему и как работает `@MainActor` вы узнаете совсем скоро, а пока очень важно запомнить, что весь код, работающий с UI автоматически переключается на главную очередь. Однако есть нюанс — это работает только в случае использования исключительно новой многопоточности. Если же вы вызвали функцию с замыканием (completion handler), то автоматического перехода на главную очередь не произойдёт. Даже если обернуть такой код внутрь `Task`.
Почему и как работает `@MainActor` вы узнаете совсем скоро, а пока очень важно запомнить, что весь код, работающий с UI, автоматически переключается на главную очередь. Однако есть нюанс — это работает только в случае использования исключительно новой многопоточности. Если же вы вызвали функцию с замыканием (completion handler), то автоматического перехода на главную очередь не произойдёт. Даже если обернуть такой код внутрь `Task`.
```
```swift
override func viewDidLoad() {
super.viewDidLoad()
@@ -59,7 +61,7 @@ private func didFailLoadImage(_ error: Error) {
```
# Отмена задач
## Отмена задач
Задача не может быть завершена, пока не будут завершены все подзадачи. У `Task` есть функция `cancel()`, которая отвечает за завершение задачи. Однако сам по себе вызов `cancel()` не завершает задачу, а лишь устанавливает флаг `isCancelled` в значение true.
@@ -67,7 +69,7 @@ private func didFailLoadImage(_ error: Error) {
> Ваша задача — следить за состоянием задачи с помощью вызова функции `Task.checkCancellation()` или проверки флага `isCancelled`. В случае, если задача была завершена, вызов `Task.checkCancellation()` выкинет ошибку `CancellationError`.
```
```swift
func loadStrings() async {
let loadTask = Task { () -> [String] in
let url = URL(string: "https://...")!
@@ -94,7 +96,7 @@ func loadStrings() async {
Но что, если у вас внутри задачи есть подзадачи и в случае отмены родительской задачи нужно отменить все сабтаски?
```
```swift
let mainTask = Task {
Task {
// ...
@@ -108,8 +110,8 @@ mainTask.cancel()
В этом случае вызов `cancel()` отменит лишь `mainTask`, проигнорировав две дочерние задачи. Вызвано это тем, что создание задач является частью не структурированного программирования, и нет иерархии связи между задачами и подзадачами. Говоря простым языком, в этом случае вы создаёте не подзадачи, а две новые задачи верхнего уровня (задачи без родителя). Для решения такой задачи можно использовать `withTaskCancellationHandler`.
```
let mainTask = Task {
```swift
let mainTask = Task {
let task1 = Task {
// ...
}
@@ -130,13 +132,13 @@ mainTask.cancel()
Такое решение имеет право на жизнь, однако вы теряете преимущества структуриванного подхода, и код становится громоздким. Вторым способом решения задачи с иерархией задач является использование `TaskGroup`.
# TaskGroup
## TaskGroup
Одним из преимуществ структурированной многопоточности является возможность построения зависимостей между задачами, поддерживаемых `Swift`. В таком случае при отмене родительской задачи по иерархии будут оповещениы все сабтаски.
Давайте разберём на примере выше:
```
```swift
let mainTask = Task {
await withTaskGroup(of: Void.self) { group in
group.addTask {
@@ -155,7 +157,7 @@ let mainTask = Task {
> Группы задач используются не только для удобного механизма отмены, но и для ситуаций, когда нужно параллельно выполнить несколько задач.
```
```swift
await withTaskGroup(of: String.self) { group in
let albumsIDs = await albums(singer: "Frank Sinatra")
for album in albumsIDs {
@@ -168,9 +170,9 @@ await withTaskGroup(of: String.self) { group in
- после чего для каждого из альбомов создали и запустили таску по скачиванию песен в заданном альбоме;
- если у исполнителя 4 альбома, то задачи будет тоже 4, если же альбомов больше, то и задач, соответственно, будет больше.
Ещё один кейс использования Task Group — необходимость вернуть итоговый массив данных.
Ещё один кейс использования `Task Group` — необходимость вернуть итоговый массив данных.
```
```swift
let songs = await withTaskGroup(of: String.self, returning: [String].self) { group in
let albumsIDs = await albums(singer: "Frank Sinatra")
for album in albumsIDs {
@@ -191,7 +193,7 @@ let songs = await withTaskGroup(of: String.self, returning: [String].self) { gro
Если же в процессе загрузки может произойти ошибка, то возможно использование группы с `try`:
```
```swift
let songs = try await withThrowingTaskGroup(of: String.self, returning: [String].self) { group in
let albumsIDs = try await albums(singer: "Frank Sinatra")
for album in albumsIDs {
@@ -211,21 +213,23 @@ let songs = try await withThrowingTaskGroup(of: String.self, returning: [String]
> Также при работе с `TaskGroup`, может быть полезно использовать функцию `next()`, которая возвращает результат тасок в порядке их выполнения.
В примере ниже, мы возвращаем результат той задачи, которая будет выполнена раньше остальных. Остальные таски будут отменены:
```
```swift
let song = await withThrowingTaskGroup(of: String.self, returning: String.self) { group in
let albumsIDs = await albums(singer: "Frank Sinatra")
for album in albumsIDs {
group.addTask { try await songs(in: album) }
}
let song = try await group.next()
return song
guard let first = try await group.next() else { throw SomeError.noResults }
group.cancelAll()
return first
}
```
Для отмены всех задач внутри группы необходимо использовать функцию `cancelAll()`. Если после вызова `cancelAll()` в группу будет добавлена новая задача с помощью `addTask()`, новой задаче сразу будет присвоен статус отменённой. Однако если сама задача не поддерживает отмену с помощью проверки флага `isCancelled` или `Task.checkCancellation()`, то задача всё равно будет выполнена. В качестве альтернативы, чтобы избежать выполнения задач в группе после вызова `cancelAll()`, можно добавлять таски с помощью `addTaskUnlessCancelled()`. Если группа была завершена, то `addTaskUnlessCancelled()` вернёт `false`.
# Detached Task
## Detached Task
Detached Task используются в ситуациях, когда необходимо создать новую задачу верхнего уровня, «оторвав» её от родительского контекста. На практике Detached Task используются редко, так как имеют большие накладные расходы (overhead), чем использование Task. Создаются такие задачи с помощью метода detached, в качестве параметра возможно передавать приоритет задачи.
@@ -239,6 +243,6 @@ Apple рекомендует избегать использование `detach
# Подведём итоги
В рамках этого урока вы познакомились с Task и TaskGroup, их синтаксисом и использованием. Также напоминаем вам, что async let под капотом создают новую таску с контекстом родителя. В рамках этого урока вы научились отменять задачи и узнали, как создать новую задачу верхнего уровня и какие риски это несёт в себе.
В рамках этого урока вы познакомились с `Task` и `TaskGroup`, их синтаксисом и использованием. Также напоминаем вам, что async let под капотом создают новую таску с контекстом родителя. В рамках этого урока вы научились отменять задачи и узнали, как создать новую задачу верхнего уровня и какие риски это несёт в себе.
Дальше вы продолжите погружаться в мир новой многопоточности и узнаете, что в Swift теперь есть способ верификации корректности асинхронного кода (`@Sendable`), а также что помимо классов и структур появился новый тип — акторы (`actor`) и про упомянутый выше `@MainActor`!
@@ -2,19 +2,18 @@
На прошлом уроке вы узнали о новом типе асинхронности в Swift — `async-await`. Этот механизм, а также аналогичные ему механизмы в других языках программирования, позволяют писать асинхронный код так, будто он синхронный. Таким образом можно избежать использования колбэков (callbacks) и делать код более читаемым и понятным.
> Здесь и далее мы будем указывать английский термин в скобках, если для соответствующего понятия нет устоявшегосярусскоязычного термина. Например, «колбэк» может быть переведён как «обратный вызов», и его использование в этом контексте могло привести к путанице.
> Здесь и далее мы будем указывать английский термин в скобках, если для соответствующего понятия нет устоявшегося русскоязычного термина. Например, «колбэк» может быть переведён как «обратный вызов», и его использование в этом контексте могло привести к путанице.
Также вы познакомились с понятием структурированной асинхронности (structured concurrency) и новыми инструментами для работы с многопоточностью в Swift 5.5: `Task`, `TaskGroup`, `async let`, `withTaskGroup`, `withThrowingTaskGroup`, и другими. Они позволяют более точно описать структуру выполнения асинхронного кода для достижения большей эффективности. Например, используя `async-let` вы можете запустить несколько асинхронных задач _параллельно_ в случае, если количество задач известно на этапе компиляции. Если же количество задач неизвестно заранее, то можно использовать `TaskGroup` или `withTaskGroup`.
Помимо структурированной асинхронности, в Swift 5.5 были введены новые инструменты для работы с многопоточностью: протокол `Sendable` и акторы (`actor`, `@MainActor`).
О них мы поговорим в следующих уроках. А в уроке мы углубимся в мир асинхронности: узнаем про разделяемые и изолированные данные, контекст выполнения
и основные классы ошибок, возможных при работе в многопоточной среде.
О них мы поговорим в следующих уроках. А в этом уроке мы углубимся в мир асинхронности: узнаем про разделяемые и изолированные данные, контекст выполнения и основные классы ошибок, возможных при работе в многопоточной среде.
КНОПКА
Скорее вперёд!
# Преимущества `async-await`
## Преимущества `async-await`
- Повышение читабельности. Асинхронный код, который выглядит как синхронный, упрощает понимание кода и снижает вероятность ошибок.
- Простая и надёжная обработка ошибок. Использование конструкции try-catch гарантирует, что ошибки не останутся незамеченными.
@@ -24,7 +23,7 @@
# Недостатки `async-await`
- Более высокий порог входа. Например, важно помнить, что после вызова `await` выполнение кода может быть продолжено в другом потоке, и состояние приложения может измениться. Такого рода ошибку легко допустить в виду кажущейся последовательности инструкций в коде. Однако если упустить этот момент, то во время выполнения программы могут появиться трудно обнаруживаемые ошибки.
- Более высокий порог входа. Например, важно помнить, что после вызова `await` выполнение кода теоретически может быть продолжено в другом потоке, и состояние приложения может измениться. Такого рода ошибку легко допустить в виду кажущейся последовательности инструкций в коде. Однако если упустить этот момент, то во время выполнения программы могут появиться трудно обнаруживаемые ошибки.
- Дополнительные накладные расходы (overhead). При использовании `async-await` тратятся ресурсы на создание и управление задачами, переключение контекста и т.д. Часть переменных и объектов, которые раньше можно было хранить на стеке, теперь приходится хранить в куче (heap). Переключение контекста между задачами также требует дополнительных ресурсов хоть и стоит дешевле, чем в случае использования потоков (threads).
Таким образом, `async-await` хоть и упрощает написание асинхронного кода, но требует от разработчика более глубокого понимания многопоточности и внимательного проектирования.
@@ -43,7 +42,7 @@
## Классы ошибок, возможных при работе в многопоточной среде
Ниже мы перечислим основные классы ошибок, которые могут возникать при работе в многопоточной среде. Более подробно обсудим некоторые из них уже в этом уроке.
Ниже мы перечислим основные классы ошибок, которые могут возникать при работе в многопоточной среде. Более подробно обсудим некоторые из них в следующих уроках.
- **Гонки данных (data race)** — это ситуация, когда несколько потоков обращаются к одним и тем же общим данным, и хотя бы одно из обращений является изменением этих данных.
- **Состояния гонки (race condition)** — это более общий тип ошибок, чем гонки данных. Корректность работы программы зависит от порядка выполнения операций. Гонки данных являются частью состояний гонки.
@@ -60,10 +59,10 @@
Также существует множество ошибок, связанных с асинхронностью, которые зависят от конкретного языка программирования или среды выполнения.
# Подведём итоги
## Подведём итоги
В Swift 5.5 были предприняты шаги для уменьшения вероятности возникновения многих из таких ошибок, такие как введение `Sendable`, `actor`, `@MainActor` и других инструментов. Однако использование этих инструментов не гарантирует отсутствие всех ошибок.
В Swift 5.5 и выше были предприняты шаги для уменьшения вероятности возникновения многих из таких ошибок, такие как введение `Sendable`, `actor`, `@MainActor` и других инструментов. Однако использование этих инструментов не гарантирует отсутствие всех ошибок.
Более того, **некоторые классы ошибок вообще не могут быть обнаружены компилятором**: например, состояния гонки в общем случае могут быть обнаружены только во время выполнения программы (runtime).
В следующем уроке мы поговорим о новом типе `Sendable` и о том, как он помогает обеспечить безопасность при работе с многопоточностью в Swift 5.5.
В следующем уроке мы поговорим о новом типе `Sendable` и о том, как он помогает обеспечить безопасность при работе с многопоточностью.
@@ -1,5 +1,7 @@
# Sendable типы и функции
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В прошлом уроке мы поговорили про плюсы и минусы механизма `async-await`; коснулись важной темы разделяемых и изолированных данных, контекста выполнения; а также обозначили основные классы ошибок, возникающих в асинхронном коде. В этом уроке мы познакомимся с новым протоколом `Sendable`, который позволяет пометить тип данных как безопасный для передачи между потоками.
КНОПКА
@@ -7,6 +9,8 @@
Итак, в Swift 5.5 (iOS 13.0+) добавлен протокол `Sendable` для маркировки типов, которые могут быть безопасно переданы между потоками (а точнее, доменами изоляции) с помощью копирования значений. Протокол `Sendable` позволяет Swift выполнять проверки корректности реализации асинхронного кода во время компиляции.
> Начиная с Swift 6, соблюдение `Sendable` становится обязательным для корректной работы с асинхронным кодом. Несоответствие может приводить не к предупреждениям, а к ошибкам компиляции.
О том, что такое «домены изоляции» мы поговорим ниже. Но для начала нужно будет ввести некоторые базовые понятия.
# Состояния, мутации, изоляция состояния
@@ -172,7 +176,7 @@ extension Task {
Мы видим, что `Task.detached` ожидает, что блок `operation` будет `@Sendable`, то есть его можно передавать между потоками. Компилятор Swift должен гарантировать, что блок `operation`, удовлетворяет протоколу `Sendable`.
КНОПКА - диалг
КНОПКА - диалог
Студент: Как он это делает?
Практикум: Компилятор считает, что блок удовлетворяет протоколу `Sendable`, если все его параметры, захватываемые значения и возвращаемое значение удовлетворяют протоколу `Sendable`.
@@ -226,7 +230,7 @@ Task.detached {
- все его свойства либо неизменяемы, либо удовлетворяли протоколу `Sendable`;
- является подклассом `NSObject`, либо не имеет суперкласса.
> Классы, которые помечены `@MainActor`, также удовлетворяют протоколу `Sendable`. О том, что такое `@MainActor` и как его использовать, мы поговорим в 4-м уроке.
> Классы, которые помечены `@MainActor`, также удовлетворяют протоколу `Sendable`. О том, что такое `@MainActor` и как его использовать, мы поговорим в одном из следующих уроков.
Классы, которые не удовлетворяют ни одному из условий выше, могут быть помечены атрибутом `@unchecked Sendable`, если вы уверены, что они безопасны для использования в многопоточной среде. Такая пометка отключит проверки компилятором Swift и в вашей ответственности будет убедиться, что класс действительно изолирован.
@@ -234,7 +238,7 @@ Task.detached {
Вместо того чтобы удовлетворять протоколу `Sendable`, функции и замыкания помечают атрибутом `@Sendable`, чтобы обозначить, что они безопасны для использования в многопоточной среде. При этом любое значение, которое захватывает функция или замыкание, должно удовлетворять протоколу `Sendable`.
> `Sendable` замыкания могу захватывать переменные [только по значению (value-capturing)](https://developer.apple.com/documentation/swift/sendable#Sendable-Functions-and-Closures). То есть `Sendable` замыкание не должно мутировать захваченные переменные.
> `Sendable` замыкания могут захватывать переменные [только по значению (value-capturing)](https://developer.apple.com/documentation/swift/sendable#Sendable-Functions-and-Closures). То есть `Sendable` замыкание не должно мутировать захваченные переменные.
```swift
let sendableClosure = { @Sendable (number: Int) -> String in
@@ -276,7 +280,7 @@ Xcode 14+ позволяет управлять строгостью прове
- **(Minimal) Минимальный**: Компилятор будет диагностировать только экземпляры, явно помеченные как соответствующие `Sendable`;
- **(Target) Целевой**: Применяет ограничения `Sendable` и выполняет проверку изоляции для всего кода, который использует новыt механизмы асинхронности, такие как `Task`, `async/await`, `async-let`. Компилятор также будет проверять экземпляры, которые явно принимают `Sendable`. Этот режим пытается найти баланс между совместимостью с существующим кодом и выявлением потенциальных гонок данных.
- **(Target) Целевой**: Применяет ограничения `Sendable` и выполняет проверку изоляции для всего кода, который использует новые механизмы асинхронности, такие как `Task`, `async/await`, `async-let`. Компилятор также будет проверять экземпляры, которые явно принимают `Sendable`. Этот режим пытается найти баланс между совместимостью с существующим кодом и выявлением потенциальных гонок данных.
- **(Complete) Полный**: Соответствует предполагаемой семантике Swift 6 для проверки и устранения гонок данных. Этот режим проверяет всё, что делают два других режима, но выполняет эти проверки для всего кода в вашем проекте.
@@ -310,7 +314,7 @@ final class User {
}
```
B модификтор `.task` для вашей `View` добавьте следующий код:
B модификатор `.task` для вашей `View` добавьте следующий код:
```swift
.task {
@@ -453,6 +457,6 @@ final class User: @unchecked Sendable {
- Настройку `SWIFT_STRICT_CONCURRENCY` для проверки асинхронного кода;
- Практическое использование `Sendable` для безопасного доступа к изменяемому состоянию объектов.
Вы далеко в понимании асинхронного программирования на Swift!
Вы далеко продвинулись в понимании асинхронного программирования на Swift!
В следующем уроке мы поговорим о новом типе данных — акторах (actors), которые обеспечивают безопасный доступ к изменяемому состоянию. Все акторы по умолчанию удовлетворяют протоколу `Sendable`.
@@ -1,12 +1,16 @@
# Акторы
В прошлом уроке мы рассмотрели протокол `Sendable`, который маркирует типы, которые можно передавать между потоками без верояности гонок данных. Компилятор Swift использует протокол `Sendable`, чтобы проврять отсутствие гонок данных в коде и предотвращать ошибки. Чаще всего он используется в связке с акторами. В этом уроке мы как раз и узнаем про новый ссылочный тип — `actor`, и как он используется в Swift.
> Код, представленный в этом уроке, проверен на Xcode 16.2. Версии, которые появятся позже, могут привнести изменения, приводящие к ошибкам компиляции или предупреждениям.
В прошлом уроке мы рассмотрели протокол `Sendable`, который маркирует типы, которые можно передавать между потоками без верояности гонок данных. Компилятор Swift использует протокол `Sendable`, чтобы проверять отсутствие гонок данных в коде и предотвращать ошибки. Чаще всего он используется в связке с акторами. В этом уроке мы как раз и узнаем про ссылочный тип — `actor`, и как он используется в Swift.
> Акторы — это объекты, которые обеспечивают изоляцию _своего_ состояния.
Одним из хороших примеров использования акторов в Swift является реализация сервисных классов, например, загрузчика картинок или сервиса работы с базой данных. Например, реализовав сервис работы с банковским балансом в виде актора, мы можем гарантировать, что доступ и модификация к балансу будет происходить атомарно. Ещё пример: это реализация сервисных классов, таких как загрузчик картинок или сервис работы с базой данных.
> Если мы реализуем сервис работы с банковским балансом в виде актора, то можем гарантировать, что доступ и изменение баланса будут происходить безопасно и атомарно. Атомарное выполнение методов актора означает, что его выполнение не будет прервано другими потоками.
Если мы реализуем сервис работы с банковским балансом в виде актора, то можем гарантировать, что доступ и изменение баланса будут происходить безопасно и атомарно. Атомарное выполнение методов актора означает, что его выполнение не будет прервано другими потоками.
> Начиная с Swift 5.9 и особенно в Swift 6, все значения, которые передаются в актор (внутрь или из него), должны соответствовать Sendable. В противном случае компилятор выдаст ошибку. Это особенно важно при работе с массивами, словарями или типами, которые содержат ссылочные значения.
## Синтаксис объявления акторов
@@ -111,7 +115,7 @@ actor User: Identifiable {
При взаимодействии 2-х или более акторов мы всё ещё можем получить _ситуацию гонки_. В сессии [WWDC 2022 Eliminate data races using Swift Concurrency](https://developer.apple.com/wwdc22/110351) был приведён такой пример: пусть у нас есть 2 актора `Ship` (корабль) и `Island` (остров). Корабли могут перевозить грузы на борту. Пусть они перевозят ананасы. Также корабли могут временно оставлять свой груз на острове для хранения.
(Тут для наглядности мы несколько упростили исходные прмеры из сессии WWDC 2022)
(Тут для наглядности мы несколько упростили исходные примеры из сессии WWDC 2022)
```swift
struct Pineapple {
@@ -190,4 +194,4 @@ await island.updatePinaples { pinaples in
# Подведём итоги
Акторы — это новый инструмент в Swift 5.5 для обеспечения безопасности при работе с многопоточностью. Они позволяют изолировать доступ к изменяемому состоянию и гарантируют, что доступ к этому состоянию будет происходить атомарно. Однако акторы не исключают ситуаций гонки, и важно понимать, что безопасность параллельного доступа к данным требует внимательного проектирования и анализа.
Акторы — это инструмент, появившийся в Swift 5.5 для обеспечения безопасности при работе с многопоточностью. Они позволяют изолировать доступ к изменяемому состоянию и гарантируют, что доступ к этому состоянию будет происходить атомарно. Однако акторы не исключают ситуаций гонки, и важно понимать, что безопасность параллельного доступа к данным требует внимательного проектирования и анализа.
@@ -1,3 +1,5 @@
# MainActor
В прошлом уроке мы познакомились с акторами и их использованием в Swift. В этом уроке рассмотрим особый тип актора — `MainActor`.
Кнопка
@@ -84,7 +86,7 @@ Task.detached(priority: .background) {
Типы, помеченные как `@MainActor`, могут автоматически быть помечены как `Sendable`.
# Подведём итоги
## Подведём итоги
`MainActor` — это важный инструмент для работы с пользовательским интерфейсом в многопоточной среде. Часто `@MainActor` помечают View Model в MVVM — это позволяет гарантировать корректность работы с пользовательским интерфейсом.
@@ -1,3 +1,5 @@
# Проверим изученное
В прошлых уроках мы изучили обширный материал по работе с асинхронностью в Swift 5.5+. В этом уроке мы закрепим полученные знания.
Мы пройдёмся по следующим темам:
@@ -6,11 +8,11 @@
- Континуации (`withCheckedContinuation` и д.р.);
- Использование `@Sendable`;
- Акторы (`actor`, `@MainActor`);
- Повторим возмозные проблемы, возникающие в асинхронном коде (гонки данных, ситуации гонки, взаимные блокировки и т.д.).
- Повторим возможные проблемы, возникающие в асинхронном коде (гонки данных, ситуации гонки, взаимные блокировки и т.д.).
# Структурированная асинхронность
## Структурированная асинхронность
## `async` и `await`
### `async` и `await`
Повтороим кратко, как работают `async` и `await`.
@@ -37,7 +39,7 @@ Task {
}
```
## `async-let`
### `async-let`
Вызов `await` приостанавливает выполнение текущей функции и ожидает завершения асинхронной операции. Но что, если нужно выполнить несколько асинхронных операций _параллельно_ и дождаться их завершения? Для этого используется `async-let`.
@@ -50,7 +52,7 @@ let images = await [image1, image2] // выполнение текущей за
`async let` можно использовать только внутри асинхронной функции. Жизненный цикл переменной, объявленной с `async let`, ограничен областью видимости текущей функции.
## Группы задач
### Группы задач
Мы используем `async-let` в случае, если количество параллельных задач известно заранее на этапе компиляции. Но если нужно создать _динамический_ параллелизм, то есть разное количество задач и их количество определеяется во время выполнения программы, то используются группы задач.
@@ -72,9 +74,9 @@ await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
Отменить все задачи в группе можно с помощью `cancellAll()`.
# Неструктурированная асинхронность
## Неструктурированная асинхронность
## `Task`
### `Task`
`Task` — это единица асинхронной работы. При создании в `Task` передаётся `@Sendable` замыкание, которое будет выполнено асинхронно.
@@ -105,7 +107,7 @@ Task(proirity: .userInitiated) { // Задача 1
Задачи можно отменять, если результат выполнения уже не нужен. Например, если пользователь закрыл экран, на котором происходит загрузка данных, то перед закрытием экрана обычно имеет смысл отменить все задачи, связанные с этим экраном.
> Важно: отмена задачи не означает, что она будет прервана немедленно. Она будет завершена, но результат выполнения не будет использован.
> Важно: отмена задачи не означает, что она будет прервана немедленно. Она будет завершена, но результат выполнения не будет использован. Если у задачи есть дочерние, они также будут отменены.
Для отмены задачи используется метод `cancel()`:
@@ -121,7 +123,7 @@ let task = Task {
}
```
## `Task.detached`
### `Task.detached`
Если нужно создать задачу, которая не будет зависеть от текущего контекста выполнения, то используется `Task.detached`:
@@ -134,7 +136,7 @@ Task.detached {
Отвязанные задачи не наследуют приоритет выполнения и не будут отменены в случае отмены задачи, из которой мы создали `detached` задачу.
# Континуации
## Континуации
Если нужно из функции с колбеком сделать `async` функцию — мы используем континуации:
@@ -157,7 +159,7 @@ func fetchImage(url: URL) async throws -> UIImage {
}
```
# Использование `@Sendable`
## Использование `@Sendable`
`@Sendable` — маркерный протокол, который обозначет, что тип или замыкание можно безопасно передавать между потоками (точнее, контекстами выполнения).
@@ -167,13 +169,13 @@ func fetchImage(url: URL) async throws -> UIImage {
Более подробно смотрите шестой урок данной темы.
# Акторы
## Акторы
В Swift 5.5 добавлен новый ссылочный тип `actor`, который изолирует внутреннее состояние. Использование акторов позволяет избежать гонок данных, но при этом ситуации гонки все ещё возможны. Более подробно смотрите 3-й урок данной темы.
В Swift 5.5 добавлен новый ссылочный тип `actor`, который изолирует внутреннее состояние. Использование акторов позволяет избежать гонок данных, но при этом ситуации гонки все ещё возможны. Более подробно смотрите 7-й урок данной темы.
Среди всех акторов выделяется — главный актор `@MainActor`, который связан с главным потоком. Класс помеченный как `@MainActor` гарантирует, что доступ к его свойствам и методам будет происходить только из главного потока. Более подробно смотрите 8-й урок данной темы.
# Порешаем задачи и квизы
## Порешаем задачи и квизы
Ниже представлены задачи и квизы для закрепления материала. Некоторые задачи имеют форму квиза с вариантами ответов. В других вам нужно будет найти ошибку или написать код. Задачи идут в порядке увеличения их сложности. Для решения каждой задачи вам могут понадобиться знания из всех описанных выше тем. Если задача сложная, то мы в ней это отметим и ее можно отложить, но всё же желательно вернуться к ней позже.
@@ -1,6 +1,6 @@
# Финал проекта. Итоговый обзор проекта "Расписание Путешествий": что мы изучили
В завершающем уроке мы вкратце повторим все новые понятия, которые были изучены в этом модуле. Проектом этого модуля было многоэкранное приложение на SwiftUI "Расписание Путешествий". Этот проект позволил вам освоить вёрстку, навигацию и анимации на SwiftUI, реализовать архитектуру MVVM на SwiftUI с использованием Combine, а так же использовать Combine для асинхронных вызовов, поработать со структурированной многопоточностью.
В завершающем уроке мы вкратце повторим все новые понятия, которые были изучены в этом модуле. Проектом этого модуля было "Расписание Путешествий". Этот проект позволил вам освоить вёрстку, навигацию и анимации на SwiftUI, реализовать архитектуру MVVM на SwiftUI с использованием Combine, а так же использовать Combine для асинхронных вызовов, поработать со структурированной многопоточностью.
## В этом модуле
@@ -45,5 +45,3 @@
Вы почти сделали ещё одно приложение! Остался последний шаг - связать между собой сетевой слой и графический интерфейс приложения для полного завершения проекта.
Это был насыщенный модуль, полный новых передовых фреймворков и новых подходов. Знания, полученные в этом модуле, при создании проекта "Расписание Путешествий", ещё лучше и больше подготовили вас к реальным задачам мобильной разработки.
[Кнопка: Продолжить обучение]
@@ -1,3 +1,5 @@
# Сдаём задачу спринта 21 на ревью
Вы завершили 21 спринт, в котором научились реализовывать MVVM на SwiftUI + Combine и узнали про структурированную многопоточность (Swift Structured Concurrency). Пора приступать к заданию для самостоятельной работы!
Вам предстоит связать между собой сетевой слой и графический интрефейс приложения и тем самым завершить ваш проект «Расписание путешествий».
@@ -30,12 +32,14 @@
## Чек-лист
- [ ] Созданы `ViewModel` для каждого экрана приложения.
- [ ] Добавлен модификатор `task` для вызова метода у `ViewModel` (там, где это необходимо).
- [ ] Сетевой клиент является `actor` и имеет методы для вызова каждого из сервисов.
- [ ] Все методы сетевого клиента асинхронные (`async`) и возвращают модели данных.
- [ ] Структуры, передаваемые между контекстами выполнения, помечены атрибутом `@Sendable`.
- [ ] В настройках билда (Xcode PROJECT -> Build Settings) для переменной `SWIFT_STRICT_CONCURRENCY` установлено значение `Complete`.
- [ ] Используется Structured Concurrency.
- [ ] Методы, использующие callback-функции, обёрнуты в асинхронные методы с помощью `withCheckedContinuation`.
- [ ] Приложение работает с API «Яндекс Расписаний» и протестировано перед сдачей на ревью.
- Убедитесь, что пулл-реквесты предыдущих спринтов смержены в main.
- Созданы `ViewModel` для каждого экрана приложения.
- Добавлен модификатор `task` для вызова метода у `ViewModel` (там, где это необходимо).
- Сетевой клиент является `actor` и имеет методы для вызова каждого из сервисов.
- Все методы сетевого клиента асинхронные (`async`) и возвращают модели данных.
- Структуры, передаваемые между контекстами выполнения, помечены атрибутом `@Sendable`.
- В настройках билда (Xcode PROJECT -> Build Settings) для переменной `SWIFT_STRICT_CONCURRENCY` установлено значение `Complete`.
- Используется Structured Concurrency.
- Методы, использующие callback-функции, обёрнуты в асинхронные методы с помощью `withCheckedContinuation`.
- Приложение работает с API «Яндекс Расписаний» и протестировано перед сдачей на ревью.
- Дизайн проекта полностью соответствует дизайну макета в Figma, включая все элементы, шрифты, цвета и отступы.
@@ -0,0 +1,226 @@
# Жизненный цикл продукта. Работа с App Store
Вы завершили работу над очередным проектом курса. Поздравляем! Сейчас вы здесь:
![](https://pictures.s3.yandex.net:443/resources/Zhiznennyi_tsikl_produkta_1697034342.png)
После этого спринта вы приступите к работе над дипломным проектом. Этот спринт подготовительный и поэтому не совсем обычный. Он продлится *неделю*. В этом спринте:
- вы узнаете о процессе разработки продукта и инструментах для командной работы — это поможет подготовиться к разработке в группе;
- познакомитесь с дипломным проектом;
- разберётесь, как декомпозировать задачи;
- посмотрите на примере, как будет выглядеть ваша работа в течение следующих спринтов.
В этом уроке мы поговорим о мобильном приложении как о продукте, а также расскажем про его жизненный цикл.
Знание жизненного цикла продукта и тонкостей его создания поможет вам принимать эффективные решения. Ведь чтобы стать результативными разработчиками, нужно не только уметь писать качественный код и разбираться в технологиях — следует понимать, как создаётся продукт, и осознавать свою роль на каждом этапе. Это позволит уменьшить количество потенциальных проблем.
А ещё понимание процессов позволит вам эффективнее взаимодействовать с коллегами. Ваш успех во многом зависит от умения налаживать сотрудничество с разными специалистами как внутри своей команды, так и вовне.
Современные процессы разработки достаточно гибки. Начальные требования или заложенные в продукт гипотезы могут меняться, из-за чего приходится переписывать код. Открытость к изменениям и навык быстрого включения в новые запросы — важные элементы командной работы!
**Кнопка-реплика**
Понятно! А что же такое продукт?
Обычно под продуктом понимается мобильное приложение. Однако в широком смысле продуктом может быть **любой программный компонент, обладающий самостоятельной ценностью для пользователей**. Поэтому большое мобильное приложение можно представить как набор продуктов. Каждый из них управляется менеджером продукта и создаётся отдельной командой разработки.
Примеры продуктов:
- мобильная игра;
- библиотека, поддерживающая функциональность чата;
- библиотека компонентов пользовательского интерфейса;
- кнопка Apple Pay с функциональностью платежей в приложении.
> Продуктовый подход в мобильной разработке — это:
>
> - *поиск продукта*, то есть той функциональности, которая решает задачи пользователей, закрывает их «боли»;
> - *упаковка* этого продукта в мобильное приложение.
**Кнопка-вопрос**
Что такое жизненный цикл продукта?
Это отрезок времени, который начинается с момента принятия решения создать продукт и длится до полного прекращения его использования. Создание и развитие успешного продукта — сложный и многоступенчатый процесс. Он происходит постепенно и содержит ряд обязательных шагов, часть которых можно выполнять параллельно.
> Иногда жизненный цикл продукта предопределён. Например, заранее известен срок жизни мобильного приложения, разработанного для гостей Олимпийских игр. Но в большинстве случаев жизненный цикл продукта заканчивается, когда проект решают закрыть. А это может произойти на любом этапе.
После выхода каждой новой версии разработка, как правило, не останавливается — начинается новый цикл:
![](https://pictures.s3.yandex.net:443/resources/product_lifecycle_1681998003.png)
Здесь мы видим, что:
- продукт развивается поэтапно (в Agile этапом является спринт);
- каждый этап состоит из нескольких фаз: от требований до релиза новой версии и разбора результатов;
- результаты выхода очередной версии — основа для нового этапа разработки;
- количество этапов не определено — жизненный цикл может завершиться на любом этапе.
Давайте разберёмся в этих фазах подробнее!
КНОПКА
Вперёд!
## Фаза 1: требования
На этой фазе команда работает с идеей продукта или с гипотезой о том, какая функциональность будет ценной для пользователя. Если продукт платный, то оценивают, готовы ли пользователи платить за продукт.
Уже на этом этапе нередко создают прототип продукта. Прототипы могут делать как дизайнеры (макеты экранов, переходы между ними), так и разработчики.
Например, если в приложение требуется встроить стороннюю библиотеку чата, именно на этом этапе разработчики могут создать лёгкий прототип чата с минимумом функций. Это позволит оценить внешний вид экранов и технологию, которую планируется применить.
> Ещё одним средством проверки гипотез на этой фазе может служить A/B-тестирование. Это инструмент, который помогает точно определить, как изменения продукта повлияют на его качество.
>
> A/B-тест — это сравнение двух вариантов продукта, например двух стилей интерфейса. Во время A/B-теста аудиторию делят на две группы: одной показывают первый вариант продукта, другой — второй. После эксперимента сравнивают данные о поведении обеих групп и делают выводы.
Разработчики на первой фазе активно взаимодействуют с аналитиками, чтобы оценить реализуемость требований, понять, как их поддержать имеющимися технологиями, и предложить варианты решений. Требования к продукту на этой фазе документируют.
В результате этой фазы у команды появляется чёткое понимание того, что именно нужно разработать.
## Фаза 2: дизайн
На этой фазе команда разработки проектирует продукт. Например, дизайнеры разрабатывают для нового чата макеты экранов и его настроек. Разработчики создают архитектуру и структуру приложения: какие компоненты и библиотеки будут использоваться чатом в мобильном приложении, а какие на сервере. Чтобы в будущем оценить успешность продукта, аналитики разрабатывают показатели (метрики), которые затем будут встраиваться в систему аналитики.
Примерами метрик для чата могут быть количество нажатий на кнопку «Отправить сообщение», количество сессий пользователя в чате за день или месяц, продолжительность сессии и другие.
> Если требования, сформированные на предыдущей фазе, предполагают несколько этапов реализации, то теперь нужно запланировать задачи для каждого этапа. Для этой цели применяется {{подход backward design}}[mob_ios_backward_design].
>
> Этот подход предполагает, что конечная цель и желаемые результаты известны заранее. Зная их, команда разбивает реализацию продукта на мелкие этапы, приоритизирует их и постепенно реализует. Если в конце проекта мы хотим получить полнофункциональный чат, то сначала нужно сделать базовый обмен сообщениями, потом добавить отправку картинок, затем — аудио.
Фаза дизайна гарантирует, что приложение будет разработано с учётом потребностей пользователей.
![](https://pictures.s3.yandex.net:443/resources/232481825-b1bc3fbd-1743-4896-8684-bc781b0e53f1_1681998233.png)
## Фаза 3: разработка
На этой фазе команда разрабатывает приложение: создаёт или дорабатывает программные компоненты. Для новых продуктов частой практикой стало создание {{MVP}}[mob_ios_mvp_diploma] в качестве первой версии.
Например, в чате, который вы проектируете, нет отправки фото, видео и голосовых сообщений, нет каналов и групповых чатов. Но зато в нём есть минимально необходимая функциональность — возможность обмена сообщениями.
Результатом этой фазы станет работающий продукт, соответствующий требованиям.
## Фаза 4: тестирование
Теперь команда тестирует приложение, чтобы убедиться, что оно не содержит ошибок и работает должным образом.
В ходе тестирования продукт может пройти несколько состояний:
- Альфа-версия — рабочее, но ещё «сырое» состояние продукта.
- Бета-версия — исправлены почти все ошибки. Эту версию компания может предоставить для тестирования ограниченному кругу пользователей.
- Релиз-кандидат — почти готовая к выпуску программа.
- Релизная версия — полностью готовая программа.
Например, чат в альфа-версии может вызывать падение приложения при отсутствии сети, в бета-версии — не «оживать» при появлении сети, а в релиз-кандидате — работать адекватно в режимах онлайн и офлайн.
> Если у продукта есть внешний заказчик, на фазе тестирования происходит приёмка продукта. При необходимости создаётся документация: для других разработчиков, использующих продукт, или для заказчика. Примерами документации могут быть:
>
> - для разработчиков: описание базовых классов модуля, примеры их использования;
> - для заказчика: описание архитектуры системы, протокол интеграционного тестирования.
Разработчики на этой фазе тесно взаимодействуют с тестировщиками при поиске, анализе и исправлении ошибок.
Результатом фазы тестирования становится протестированный продукт: надёжный и соответствующий стандартам качества.
![](https://pictures.s3.yandex.net:443/resources/232481987-c37d937b-33e8-42fb-8cb9-5dbced5b246d_1681998305.png)
## Фаза 5: релиз
На этой фазе команда публикует продукт.
- Если это программный компонент для интеграции в другие приложения, он выделяется в отдельный модуль.
- Если это продукт с открытым исходным кодом — публикуется открытый код.
- А если это приложение, оно выходит в App Store. До выхода нужно зарегистрировать и оплатить аккаунт разработчика.
> Оплатить регистрацию можно только картами Visa, Mastercard, Discover или American Express. Информация о держателе карты — имя и адрес — должны в точности совпадать с данными, указанными при регистрации разработчика. Если данные не совпадут, процедура может затянуться, а Apple — потребовать дополнительные документы. После обработки платежа Apple пришлёт письмо с инструкцией по активации аккаунта.
Сейчас разработчики, которые живут в России, могут оплачивать аккаунт:
- Картами друзей или деловых партнёров, которые живут за границей, с указанием зарубежного адреса.
- Собственными картами, выпущенными в других странах.
> Контроль за приложением будет идти с аккаунта разработчика. Учтите это, если планируете использовать аккаунт, зарегистрированный на представителя за рубежом.
Теперь, когда мы разобрали оплату аккаунта, поговорим про публикацию приложения в App Store. Вот как это сделать:
1. Заполнить общую информацию о приложении: название, язык, возрастные ограничения и так далее.
2. Добавить описание версии и снимки экранов приложения.
3. Если приложение требует вход по логину и паролю — предоставить тестовые логин и пароль.
4. Отправить протестированную сборку приложения на App Review.
5. Отработать замечания Apple после ревью. Примеры замечаний: сбои в работе приложения, нарушение правил работы с персональными данными, несоответствие дизайна интерфейса требованиям {{HIG}}[mob_ios_hig].
6. После прохождения App Review опубликовать приложение нажатием на кнопку.
Давайте посмотрим, как это выглядит:
1. Для публикации приложения необходимо добавить примеры экранов приложения.
![](https://pictures.s3.yandex.net:443/resources/01_App_Preview_and_Screenshots_1691399628.png)
2. В разделе Version Information нужно внести текстовое описание приложения, описание последней версии, ключевые слова для поиска в App Store и ссылки на сайт разработчика и сайт (раздел) технической поддержки приложения.
![](https://pictures.s3.yandex.net:443/resources/02_Version_information_1691399658.png)
3. Разделы Localizable Information и General Information содержат название приложения и его краткое описание, а также технические идентификаторы приложения: Bundle ID, SKU, Apple ID.
![](https://pictures.s3.yandex.net:443/resources/03_App_information_1691399677.png)
[](https://pictures.s3.yandex.net:443/resources/04_App_general_information_1691399686.png)
4. В разделе Privacy Policy необходимо указать ссылку на политику конфиденциальности разработчика.
![](https://pictures.s3.yandex.net:443/resources/05_App_privacy_1691399714.png)
5. Раздел App Review содержит статус проверки текущей версии приложения, загруженной в App Store. В случае отказа в публикации приложения сообщение от Apple содержит причину отказа и рекомендации по дальнейшим действиям.
![](https://pictures.s3.yandex.net:443/resources/06_App_review_issues_1691399737.png)
![](https://pictures.s3.yandex.net:443/resources/07_App_review_issues_1691399746.png)
Инструкции от Apple по подготовке приложения к публикации можно прочитать на [developer.apple.com](https://developer.apple.com/help/app-store-connect/#//apple_ref/doc/uid/TP40011225-CH26-SW2), там же лежит [гайд про снимки экранов](https://developer.apple.com/help/app-store-connect/manage-app-information/upload-app-previews-and-screenshots). Со временем детали могут измениться, и это нормально, поэтому проверяйте актуальность инструкций в интернете.
Теперь разберёмся с App Review — оно начинается после загрузки новой версии приложения в App Store. На нём проходят автоматическая проверка кода и проверка специалистом-ревьюером.
В рамках автоматической проверки оценивается:
- есть ли в коде недопустимые для Apple фрагменты, например код, который не даёт оставить негативную оценку приложению в App Store;
- похож ли он на код приложения, которое используется в мошеннических целях;
- похож ли он на код приложений, которые Apple удалила из App Store из-за законодательных ограничений и санкций.
Специалист-ревьюер смотрит:
- соответствует ли приложение описанию;
- какая у приложения работоспособность.
Если какой-то этап App Review не пройден, Apple пишет разработчику причину отказа и рекомендации по дальнейшим действиям.
Причин отказа множество, но вот самые частые:
- Apple нужна дополнительная информация о приложении.
- Приложение не соответствует требованиям Apple, например Human Interface Guidelines.
- Не указано, для чего запрашивается доступ к камере, геолокации и другим инструментам.
- Контент не соответствует законодательным ограничениям или требованиям Apple.
- Сотрудник Apple не может войти в приложение и проверить его работу.
- Приложение работает нестабильно.
Причины отказа более подробно описаны на [developer.apple.com](https://developer.apple.com/app-store/review/#common-app-rejections).
## Задание со звёздочкой: опубликовать ваше приложение в App Store
> Для продвижения в App Store важен *маркетинговый план* со стратегиями по охвату целевой аудитории, информированию о функциях и преимуществах приложения. Если план составлен грамотно, вероятность успешного запуска значительно возрастёт: будет много загрузок, высокий рейтинг — и высокий доход от продукта.
На фазе релиза важно проработать доступность продукта и готовность к использованию. Результат фазы — живой продукт.
## Фаза 6: результаты
На этой фазе команда работает с результатами выпуска продукта и фиксирует полученный опыт.
Маркетологи и аналитики сопоставляют фактические и плановые результаты активности пользователей с помощью метрик аналитической системы. Оценивают, подтвердились ли гипотезы, заложенные в основу продукта.
Команда разработки поддерживает продукт: исправляет ошибки и обновляет его в соответствии с меняющимися потребностями пользователей. На этой фазе важны контроль качества продукта и регулярные выпуски обновлений. После них приложение не должно становиться хуже, чем было, наоборот — оно должно содержать исправления и улучшения.
> Если требуется передать знания о разработке и запуске продукта заинтересованным сторонам — коллегам, партнёрам, заказчикам, — то именно на этой фазе передаётся приобретённый опыт.
Результат этой фазы — обновлённый продукт и оценка заложенных в него гипотез. Она гарантирует, что продукт останется актуальным.
![](https://pictures.s3.yandex.net:443/resources/232482081-e1cfedc9-f49c-4574-b1e5-3079537709d2_1681998454.png)
## Подведём итоги
Вы познакомились с продуктовым подходом в разработке мобильных приложений: узнали, что такое жизненный цикл продукта, из каких фаз он состоит, а также какова роль разработчиков на каждой из этих фаз. Теперь вы сможете эффективнее участвовать в будущих проектах! Следуя структурированному жизненному циклу разработки, ваша команда может свести к минимуму риски, обеспечить потребности пользователей и в итоге создать успешный продукт.
@@ -0,0 +1,259 @@
# Workflow
## Обзор дипломного проекта и процесса работы
Чтобы закрепить и продемонстрировать знания, полученные на курсе, вам нужно выполнить дипломный проект. Эта работа будет отличаться от того, как вы выполняли итоговые задания спринтов. Раньше мы последовательно вели вас в разработке — урок за уроком. В этот раз вы будете работать в команде и самостоятельно реализовывать ТЗ в соответствии с нашими рекомендациями. Завершив этот проект, вы получите диплом об окончании курса «iOS-разработчик».
## О проекте. Техническое задание и дизайн
Наш дипломный проект представляет собой упрощённый вариант приложения — маркет NFT (от англ. non-fungible token, уникальные ресурсы в блокчейне, получившие известность в виде картинок). Такие маркеты стали популярны во время бума NFT пару лет назад.
> Оговоримся, что это будет хоть и приближенный к реальному, но всё-таки не совсем настоящий маркет: без биллинга (оплаты) и на моковом сервере (всегда будет возвращать заранее заданный набор данных). Мы пошли на эти шаги, чтобы размер проекта позволял реализовать его в рамках нашего курса и каждый участник команды мог разработать примерно одинаковый набор функциональности.
>
> Заметим, что отсутствие биллинга и моковый сервер никак не повлияют на ход работы над проектом. В реальных условиях разработка обычно ведётся с тестовым сервером, чтобы не засорять рабочими данными «боевой». По этой причине ваш проект не будет менее привлекательным для потенциального работодателя.
Давайте посмотрим ТЗ и дизайн, с которыми вы будете работать. Все артефакты собраны в [репозитории проекта](https://github.com/Yandex-Practicum/iOS-FakeNFT-StarterProject-Extended-Public).
Проект выглядит большим! Хорошая новость в том, что вам не нужно будет работать над ним в одиночку.
**Во-первых**, мы написали базовый код приложения, который включает `TabBarView` и сетевой слой — ту часть, которую обычно выполняют более опытные коллеги. Поэтому на рабочем месте вы, скорее всего, начнёте трудиться, имея уже написанный и похожий базовый код.
**Во-вторых**, реализовывать проект вы будете с коллегами.
**В-третьих**, часть функционала в дизайне опциональна — например, тёмная тема. Это значит, что она необязательна для получения диплома. Мы сделали эти дополнительные задания для тех, кто закончит работу над основной частью проекта раньше предусмотренного времени и захочет сделать что-нибудь ещё.
## «Скелет» проекта
![](https://pictures.s3.yandex.net:443/resources/236477567-62c3a249-7076-4f64-8f29-67cc9949c667_1683458798.png)
Часто при старте нового проекта выделяется небольшая группа опытных разработчиков, которые пишут базовый код (фреймворк, скелет) проекта. Этот код потом будут использовать остальные разработчики. Наглядный пример — сетевой клиент для запросов к серверу.
Если бы в вашем проекте такой код писал один из членов команды, то остальные были бы вынуждены ждать, пока он закончит, чтобы приступить к своей части работы.
> 👉 Поэтому мы написали этот базовый код для вас — он есть в проекте в репозитории.
Мы подготовили для вас скринкаст с обзором кода и примером созданной вкладки приложения.
> Демонстрация в скринкасте выполнена с использованием UIKit. Однако основные принципы, показанные в видео, актуальны и применимы также при разработке на SwiftUI.
[Скринкаст или встраиваемый контент](https://frontend.vh.yandex.ru/player/4a7f78654c9c89d28c4ca534651969f0?from=partner&mute=1&autoplay=0&tv=0&loop=false&play_on_visible=false)
## Работа в команде
![](https://pictures.s3.yandex.net:443/resources/236477808-16fba93c-e03a-43cd-b840-cb6528c1945e_1683458932.png)
Вы уже должны быть в команде.
> Команда состоит из 3–4 человек из вашей когорты. Работа команды будет отличаться совместным распределением задач, совместными звонками и обсуждением актуальных вопросов и проблем в разработке. А в конце вы соберёте общее приложение.
Каждый член команды будет работать над своей законченной частью функциональности в своей ветке репозитория. Приложение содержит 4 независимых вкладки:
- «Каталог»;
- «Корзина»;
- «Профиль»;
- «Статистика».
Каждый из вас будет реализовывать одну из этих вкладок. В разработке такие участки функциональности часто называют «эпиками». Только после завершения и проверки всех эпиков ревьювером вы соберёте общую финальную версию.
**Эпики не различаются по сложности**
При проектировании приложения мы руководствовались тем, чтобы сложность всех эпиков была примерно одинаковой. Какую бы вкладку вы ни разрабатывали, нужно будет работать с таблицами и коллекциями, делать сетевые запросы.
**Как будут распределяться эпики**
Вы самостоятельно распределите эпики между собой во время группового созвона с наставником.
> Скажем честно: командная разработка на учёбе может отличаться от опыта, который вы получите на работе. Мы организуем обучение так, чтобы каждый из вас разработал примерно одинаковый набор функциональности. И не попал в ситуацию, когда задачи сильно зависят от результатов другого человека: кто-то из студентов всё ещё может выпасть из командной работы.
Вот с какими элементами командной разработки вы столкнётесь:
- планирование (декомпозиция) и совместные обсуждения задач;
- синки на митингах;
- стендапы;
- ретроспективы и рефлексии;
- кросс-ревью;
- мерж работ.
По опыту предыдущих когорт, в командах, где появлялся фасилитатор — тот, кто отвечает за коммуникацию, помогает добиться целей и решить конфликты, — работа шла слаженнее, студенты получали более позитивный опыт. Поэтому мы считаем: **работа в команде станет ключом к успеху.** Вы будете помогать друг другу, учиться принимать решения вместе, а это пригодится в будущей работе.
> Команде предстоит выбрать UI-фреймворк:
>
> - или UIKit *(если вы выбрали UIKit, [скачайте соответствующее тех. задание](https://github.com/Yandex-Practicum/iOS-FakeNFT-StarterProject-Public))*;
> - или SwiftUI *(если вы выбрали SwiftUI, [скачайте соответствующее тех. задание](https://github.com/Yandex-Practicum/iOS-FakeNFT-StarterProject-Extended-Public))*.
> Крайне важно, чтобы вся команда придерживалась единого фреймворка для всего проекта. Это обеспечит консистентность кодовой базы и упростит совместную работу. Решение принимайте сообща, взвесив опыт каждого участника и специфику задач.
>
> Вот несколько аргументов для сравнения:
>
> | Аспект | UIKit | SwiftUI |
> |-----------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------|
> | **Парадигма программирования** | Императивная (вы описываете, *как* построить и изменить UI) | Декларативная (вы описываете, *каким* должен быть UI в зависимости от состояния) |
> | **Работа с UI** | Детальный контроль над каждым элементом, гибкость для сложных кастомных интерфейсов | Быстрое создание UI — особенно для стандартных экранов — с помощью меньшего количества кода |
> | **Зрелость и экосистема** | Очень зрелый, огромное количество ресурсов, библиотек, поддержка старых версий iOS | Более молодой, активно развивается, лучшая интеграция с новыми технологиями Apple |
>
> Обсудите в команде, какой фреймворк лучше соответствует вашим коллективным навыкам, целям проекта и сложности запланированных фич.
## Декомпозиция эпиков и отслеживание прогресса
Каждый эпик представляет собой довольно большую задачу, поэтому трудно оценить, сколько времени займёт его выполнение и с чего лучше начать. Эпик нужно декомпозировать, то есть разбить на более мелкие задачи, которые помогут спланировать время и сформировать план действий в виде списка.
В этом списке каждая подзадача будет описывать один небольшой шаг для реализации эпика: от создания нового класса с логикой данных до вёрстки отдельных элементов.
В реальных условиях вы можете столкнуться с разными инструментами для работы с задачами, но принципы декомпозиции задач всё равно будут одинаковы.
Классический таск-трекер организует список задач в виде досок. Он достаточно простой, чтобы быстро с ним познакомиться, при этом достаточно функциональный для работы над дипломным проектом.
В конце спринта вы примените знания на практике: декомпозируете эпик. Нужно будет оформить список задач на доске, оценить их по времени, чтобы распланировать нагрузку.
Перед тем как начать писать код, декомпозицию обычно согласовывают с менеджером проекта. Эту роль будут выполнять наставники: они помогут убедиться, что список полный, конкретный и реалистичный. После проверки и одобрения наставником можно будет приступить к работе над проектом.
Дипломный проект станет частью вашего портфолио, поэтому предлагаем сохранить декомпозицию эпика как текстовый документ в репозитории, помимо доски в таск-трекере. В портфолио не принято оставлять много ссылок на сервисы — их не любят смотреть работодатели. К тому же ссылки на сторонние сервисы иногда ломаются. Отсюда и перестраховка, которая вдобавок продемонстрирует ваш навык декомпозиции.
### Что нужно сделать
1. В таск-трекере провести декомпозицию эпика, разделив его на три равноценные и примерно одинаковые по объёму части. Наставник проверит декомпозицию и даст обратную связь. Одна часть эпика пройдёт одно ревью.
2. Создать в репозитории проекта папку `docs` и новый документ в формате Markdown (c расширением `.md`).
3. Перенести названия задач и предполагаемую оценку в часах в новый документ. Он может иметь примерно такую структуру:
```plaintext
Прочитать техническое задание — 30 минут.
Сверстать контроллер профиля — 20 минут.
...
```
## Общение с командой. Стендапы
После того как вы познакомились с командой и декомпозировали свой эпик, можно приступать к реализации задач. Так как вы будете работать в команде, мы рекомендуем ежедневно писать свой статус в командный чат: что сделано за день, что планируется сделать за следующий день, какие возникли трудности. Это поможет быть на связи с другими участниками команды, понимать общий статус проекта, а также помогать друг другу. В крайнем случае вы сможете обращаться за советом к наставнику.
В команде следует договориться о каком-то постоянном и удобном для всех времени, чтобы ежедневно собираться всем составом и обсуждать актуальные статусы каждого участника. Такие встречи есть во многих компаниях, они называются «стендапами» (от англ. stand-up).
В ходе стендапа можно обсудить такие вопросы:
- Что было сделано за прошедший период?
- С какими трудностями столкнулись?
- Что планируется сделать за следующий период?
- Есть ли какие-то препятствия, которые могут помешать выполнению задач?
- Нужна ли дополнительная помощь участникам команды для решения каких-либо задач?
Как правило, ежедневная встреча по обсуждению статусов не должна занимать больше 15 минут. Если на ней возникают вопросы, которые требуют длительного обсуждения, лучше перенести их на конец стендапа или договориться об отдельной встрече с заинтересованными участниками. Чтобы не отнимать время других, лучше заранее выписать план про то, о чём вы будете говорить, какие есть вопросы к коллегам.
Стендапы позволяют всем участникам команды понимать общую скорость команды, своевременно выявлять возникающие проблемы, поддерживать комфортный психологический климат в команде. Рекомендуем не пропускать созвоны и регулярно писать о своих проблемах и успехах в командном чате — это такая же часть работы, как и написание кода.
## Рекомендации по написанию кода
Мы подготовили для вас подробную таблицу с рекомендациями по написанию кода. Таблица большая, её сложно сразу удержать в голове, а некоторые пункты сформулированы размыто. И всё же мы считаем, что таблицу полезно прочитать и держать под рукой при написании кода. Например, после окончания каждой задачи вы можете проверять, насколько ваш код соответствует рекомендациям.
Некоторые столбцы содержат как базовый минимум для успешной сдачи диплома, так и более строгие требования. Если ваш код будет соответствовать минимальным требованиям — можете собой гордиться. Этого вполне достаточно для получения диплома и успешной работы в индустрии. Более строгие требования даны как некий идеал, к которому можно и нужно стремиться, но достижение такого на практике может потребовать слишком много времени и усилий.
> Кстати, ревьюверы будут использовать эту же таблицу для проверки.
### Рекомендации по написанию кода
В зависимости от выбранного UI-фреймворка скачайте соответствующие рекомендации:
- если вы выбрали UIKit, скачайте для него этот [чек-лист](https://code.s3.yandex.net/Mobile/iOS/pdf/...);
- если вы выбрали SwiftUI, скачайте скачайте для него этот [чек-лист](https://code.s3.yandex.net/Mobile/iOS/pdf/...).
## Работа над дополнительными задачами
Если вы закончите работу над вашим эпиком и получите одобрение ревьювера, можете выбрать одну или несколько дополнительных задач (согласовав с другими участниками команды). К таким задачам относятся:
- локализация;
- тёмная тема;
- Яндекс Метрика;
- экран авторизации;
- экран онбординга;
- алерт с предложением оценить приложение;
- сообщение о сетевых ошибках;
- launch screen;
- поиск по таблице или коллекции в своём эпике.
При работе над дополнительными задачами руководствуйтесь теми же рекомендациями, что и для основных задач.
Некоторые дополнительные задачи гораздо проще сделать, если при разработке эпика вы будете сразу писать код с прицелом на них. Для локализации — не объявлять видимые для пользователя строки в коде, а использовать `NSLocalizedString`. Для тёмной темы — задавать цвета не константами, а с использованием каталога ассетов. Даже если вы не будете выполнять дополнительные задачи, рекомендуем учитывать это при разработке эпиков — получите хорошую привычку, которая сделает вас более ценным разработчиком.
## Финальная сборка
Вы будете работать над своими задачами в отдельной ветке и сдавать Pull Request на проверку. Когда ревьювер одобрит код всех участников, понадобится слить весь код в единую ветку. И тогда вы получите готовое приложение.
![](https://pictures.s3.yandex.net:443/resources/236478775-058d26ad-a8e2-46bb-871c-36d04fd46249_1683459101.png)
Если несколько человек вносили изменения в один и тот же участок кода, то может возникнуть конфликт — ситуация, когда не получается автоматически решить, какие из этих изменений нужно оставить и в каком порядке расположить в итоговом коде. Мы подробнее расскажем вам, как решать такие конфликты, в одном из следующих уроков.
**Важно:** финальную сборку тоже необходимо сдать на ревью.
## Вехи проекта
Теперь давайте посмотрим, как будет выглядеть то, что мы обсуждали, на временной шкале, выделив ключевые моменты (вехи). Эта шкала приблизительная, сроки могут немного сдвигаться.
![](https://pictures.s3.yandex.net/resources/2977_1722425233.png)
### 24-й спринт
Напомним, в конце спринта вам нужно будет самостоятельно декомпозировать эпик и разделить его на три части. Они должны быть равноценными и примерно одинаковыми по объёму. Одна часть эпика пройдёт одно ревью.
В командном чате вы познакомитесь с наставником, выберете архитектуру проекта и способ вёрстки, распределите эпики между собой, договоритесь о регулярной коммуникации.
Наставник проверит декомпозицию в таск-трекере и текстовом документе, и вы сможете писать код с первых дней 25-го спринта.
### 25-й спринт
**Дни 12**
На встрече наставник расскажет про проект, декомпозицию эпиков, полезные сервисы и ответит на ваши вопросы.
**Дни 36**
Поработаете над **первой** частью эпика.
**День 7**
Сдадите на ревью первую часть эпика. Скажите ревьюверу, что отправляете на проверку **именно первую часть**, прикрепите Pull Request и оставьте в нём ссылку на доску в таск-трекере. В ней должна быть информация о вашем прогрессе:
- декомпозиции эпика (ссылка на таск-трекер, приложенный текстовый документ `.md`);
- первоначальной оценке времени для выполнения задач;
- реальном времени, которое вы потратили на решение каждой задачи.
> Сравните первоначальную оценку с реальным временем и поймите, насколько реалистично рассчитываете силы. Зная, насколько результат разошёлся с планом, следующую задачу вы оцените более точно.
Ревьювер оставит комментарии, которые вам нужно будет обработать к следующему ревью.
**Дни 89**
Обработаете комментарии, которые оставит ревьювер.
**Дни 1013**
Поработаете над **второй** частью эпика.
**Дни 1213**
Проведёте код-ревью: отсмотрите ветки других участников команды, а они — ваши ветки. Так вы улучшите код и научитесь работать в команде.
**День 14**
Сдадите работу на второе ревью.
### 26-й спринт
**Дни 16**
Поработаете над **третьей** частью эпика и финализируете недочёты.
**Дни 68**
Сдадите законченный эпик на ревью и получите обратную связь от ревьювера. После того как он или она одобрит работу, вы сольёте ветки в мастер-ветку и отправите её на финальное ревью. Ещё нужно будет сделать скринкаст с демонстрацией запуска проекта на симуляторе и приложить ссылку на него к Pull Request.
**Дни 711**
Пройдёт финальное ревью: ревьювер отсмотрит мастер-ветку и индивидуальные эпики.
**Дни 1215**
Состоится встреча, посвящённая рефлексии всего курса. Вы обсудите впечатления от дипломного проекта, поговорите о проблемах, с которыми столкнулись, обсудите, как можно улучшить работу.
Задача большая, работы много — это волнительно. Но мы всегда рядом и поможем вам, если будет нужно.
Вы уже знаете всё, что нужно для дипломного проекта, но в любой момент можете повторить пройденный материал. Для вас продолжат трудиться наставники и кураторы — не стесняйтесь задавать им вопросы. У вас появится новый ресурс — команда, используйте его!
Когда вы решите задачу — а мы уверены, что вы справитесь, — мы увидимся на выпускном и порадуемся за вас. Удачи!
@@ -0,0 +1,164 @@
# API
Привет! В этом уроке мы поговорим про виды серверов, FakeNFT для вашего дипломного проекта, возможности и ограничения этого сервиса. Также вы узнаете, какие инструменты помогут сохранять коллекции запросов и делиться ими. Приступим!
## Виды серверов
При работе над проектом используется несколько серверов: **продакшен** (от англ. production — «производство») и **тестовые**.
* Продакшен, или боевой сервер, принимает и обрабатывает запросы клиентов. Не рекомендуем использовать его при разработке и для поддержки, чтобы случайно не повредить данные пользователей.
* Тестовые серверы можно перезагружать, обновлять на них данные, менять логику работы, но пользователи приложения этого не увидят.
Если тестовый сервер не готов, но уже нужно начинать разработку, вас выручит **моковый** сервер. Его можно быстро запустить, в нём заранее сгенерированы тестовые данные. Для дипломного проекта мы используем наш сервис FakeNFT — давайте поговорим о нём дальше.
## FakeNFT
Мы предоставляем вам API для работы с нашим сервером. В нашем проекте нет авторизации. Базовый URL у вас будет одинаковый. Разница только в токенах авторизации, которые нужно вставить в HTTP-заголовок `X-Practicum-Mobile-Token`.
FakeNFT также поддерживает **сортировку** — всё можно настроить, если добавить параметры в запрос. Давайте рассмотрим, как это работает, на примере запроса `/users`, который отдаёт информацию о пользователях.
## Сортировка
Отсортировать данные по значению поля можно с параметром `sortBy`, его значение должно быть равно имени нужного поля.
Возьмём для примера запрос `/users`. Он возвращает список пользователей в том порядке, в котором сервер считает нужным. Вот такой ответ можно получить (для удобства чтения некоторые поля и элементы ответа пропущены, вместо них многоточие):
```swift
[
{
"name": "Carroll Cote",
"description": "learn it all",
...
},
{
"name": "Riley Glass",
"description": "Test",
...
},
{
"name": "Rayan Gosling",
"description": "easy brizi nfteasy",
...
},
...
]
```
Чтобы отсортировать его по имени пользователя `name`, добавим в запрос `sortBy`. Запрос примет вид `/users?sortBy=name`, а пользователи в ответе будут отсортированы по имени:
```swift
[
{
"name": "Abel Christensen",
"description": "daddsd",
...
},
{
"name": "Andres Stevens",
"description": "Студент Практикума\nКогорта - 14",
...
},
{
"name": "Antony Langley",
"description": "daddsd",
...
},
...
]
```
# Пагинация
Запрос `/users` возвращает данные обо всех пользователях сразу. Нам не нужен сразу весь список: отобразить их одновременно мы не сможем, они просто не уместятся на экране, значит, запрос будет впустую нагружать сервер. Чтобы решить эту проблему, мы воспользуемся пагинацией — постраничным запросом. С пагинацией мы уже сталкивались в уроке «Сетевой слой для экрана с лентой фотографий», когда изучали тему «Запрос картинок из сети».
Для того чтобы добавить пагинацию в запрос, используются параметры `page` (номер страницы, начинается с 0) и `size` (число элементов на странице).
Запрос на получение второй страницы с пользователями, по 2 пользователя на странице, будет иметь вид
`/users?page=1&size=2`:
```swift
[
{
"name": "Rayan Gosling",
"description": "easy brizi nfteasy",
...
},
{
"name": "Britney Wiley",
"description": "I'm just a gal",
...
}
]
```
Параметры можно комбинировать. Запрос на получение второй страницы **отсортированного** списка пользователей, по 2 пользователя на странице выглядит как `/users?page=1&size=2&sortBy=name`:
```swift
[
{
"name": "Antony Langley",
"description": "daddsd",
...
},
{
"name": "Arturo Larson 999999",
"description": " Ку-ку 0000000000",
...
}
]
```
Порядок параметров не важен, то есть запросы `/users?page=1&size=2&sortBy=name` и `/users?sortBy=name&page=1&size=2` вернут одинаковый результат.
## Описание API
Итак, в корне скелета проекта лежит файл `API.html`. Его можно открыть в браузере и увидеть эндпойнты, типы запросов к ним, структуру данных ответов. Чтобы получить эти данные, используем GET-запросы.
В прошлых уроках при работе с PUT-запросами вы передавали параметры так же, как в GET-запросах. На практике чаще используется другой способ — когда все параметры передаются в теле запроса, так работает и FakeNFT. Чтобы сформировать в сервисе PUT-запрос, необходимо `Content-Type` установить равным `application/x-www-form-urlencoded` и передавать данные в `body` запроса. Передавать нужно только те поля, которые обновляются.
Также в описании `API.html` есть эндпойнт `/profile/1`. Он возвращает массив `likes` — лайки пользователя. Когда пользователь ставит или убирает лайк с NFT, нужно послать PUT-запрос `/profile/1` и передать значение нового массива `likes`.
Итак, вы познакомились с API и сервисом FakeNFT, узнали о его возможностях и ограничениях. Про описание методов API, передаваемых параметров и возвращаемых значений FakeNFT можно прочитать в документации на GitHub.
Вы можете выполнить команды:
1. В консоли вызвать `git clone https://github.com/yandex-practicum-ios/fakenft.git`.
2. Открыть в скачанном репозитории `API.html`.
Дальше мы поговорим об инструменте, который поможет вам в работе.
## Postman
С помощью Postman можно отправлять тестовые запросы и проверять работу API. Это удобно, например, когда вы пишете сетевой слой и хотите проверить, что API возвращает нужные данные. [Скачайте](https://www.postman.com/downloads/) Postman с официального сайта.
В Postman также можно сохранять коллекции запросов и делиться ими. Мы подготовили коллекцию, внутри — примеры работы FakeNFT. Начните работать с ней.
1. [Скачайте](https://github.com/Yandex-Practicum/iOS-FakeNFT-StarterProject-Public/blob/main/cloud.postman_collection.json) коллекцию.
2. Откройте Postman, нажмите **Import** (⌘ + O) и выберите коллекцию.
3. Сохраните изменения в коллекции — нажмите **Save** (⌘ + S).
4. Измените ссылку на бэкенд с `https://d5d1qcn3o3c5g9qilcsn.apigw.yandexcloud.net/` на `https://d5dn3j2ouj72b0ejucbl.apigw.yandexcloud.net/`.
5. Для отправки запросов понадобится токен, который совпадает с ID пользователя. Токен можно получить у куратора.
6. Токен надо класть в HTTP-заголовок `X-Practicum-Mobile-Token`.
![Image](https://pictures.s3.yandex.net:443/resources/nft-postman_1701693965.png)
Готово! Теперь вы можете отправлять запросы и проверять ответы от FakeNFT.
> FakeNFT не контролирует согласованность передаваемых данных. Согласно API, список `nfts` для пользователя — это массив строк (`id` купленных пользователем NFT).
Чтобы изменить этот список, вы должны передать в PUT-запросе строку вида:
`{ "nfts": ["68", "69", "71"] }`
Но если вместо этого вы в PUT-запросе по ошибке передадите строку вида:
`{ "nfts": "68, 69, 71" }`
То FakeNFT сохранит эти изменения и на последующие запросы будет возвращать эту строку. Поэтому ваша задача — контролировать и корректно описывать типы данных сетевых моделей.
Если вы всё же «сломали» API, обсудите с командой шаги по его восстановлению. Скорее всего, самым простым решением будет отправка нужных PUT-запросов через Postman.
---
Вот и всё. В этом уроке вы узнали, как работать с FakeNFT, добавлять параметры в запрос и проверять API с помощью Postman. Следующая тема посвящена Git и командной работе. Мы поговорим о том, как несколько человек одновременно могут изменять фрагмент кода и как создавать ветки для новых задач.
До встречи!
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 774 KiB

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -1,38 +1,33 @@
Вы уже немного знакомы с Git, наиболее распространённой на сегодняшний день системой управления версиями. Любительскую разработку можно вести и без управления версиями — профессиональной разработке такое недопустимо, поскольку это подвергает проект огромному риску.
# Введение
Рассмотрим этот риск на примере. Представим ситуацию, в которой команда разработки работает без системы управления версиями и делает резервные копии кода каждый день. И вот команда обнаружила ошибку в коде. Чтобы её исправить, команде нужно вернуть несколько файлов в предыдущее состояние.
Вы уже немного знакомы с Git — наиболее распространённой на сегодняшний день системой управления версиями. Любительскую разработку можно вести и без управления версиями, но в профессиональной разработке такое недопустимо, поскольку это подвергает проект огромному риску.
Рассмотрим этот риск на примере. Представим ситуацию, в которой команда разработки работает без системы управления версиями и делает резервные копии кода каждый день. И вот команда обнаружила ошибку в коде. Чтобы её исправить, команде нужно вернуть несколько файлов в предыдущее состояние.
Сразу возникает много вопросов. Например:
- когда последний раз изменялся каждый из файлов: вчера, неделю назад или в другую дату? Какую резервную копию использовать?
- что делать, если разработчик вносил в них три изменения, из которых нужно восстановить второе, а резервная копия есть только для третьего, окончательного?
- что делать, если файлы меняли параллельно два разработчика и резервная копия содержит только финальную версию от первого из них, а нам нужна версия второго?
- Когда последний раз изменялся каждый из файлов: вчера, неделю назад или в другую дату? Какую резервную копию использовать?
- Что делать, если разработчик вносил в них изменения три раза, из которых нужно восстановить второе, а резервная копия есть только для третьего, окончательного?
- Что делать, если файлы меняли параллельно два разработчика и резервная копия содержит только финальную версию от первого из них, а нам нужна версия второго?
Этот список можно продолжать, но общий смысл вы поняли: без контроля версий вы рискуете:
- потерять часть кода;
- потерять много времени на восстановление работоспособности приложения;
- привнести ошибки, которые сложно исправить или откатить назад.
Практически во всех программных проектах исходный код — это ценнейший ресурс. Его надо беречь. Каждая его часть, каждый файл содержит частичку знания или представления проблемной области, которую разработчики воплощали в коде и совершенствовали. Контроль версий защищает каждую из этих частичек от ошибок и потерь во многих ситуациях. Например, таких:
- параллельное изменение фрагмента кода двумя и более разработчиками;
- параллельное изменение фрагмента кода двумя или более разработчиками;
- необходимость вернуться к прежней версии кода;
- ошибки, вызванные человеческим фактором;
- необходимость увидеть, кем и когда вносилось изменение.
Git успешно справляется с этими и другими задачами. Но практика использования вами Git в командной разработке будет, скорее всего, отличаться от вашего опыта. Именно поэтому подготовке к командной разработке мы посвятим следующие уроки. Мы познакомимся с необходимыми в командной работе возможностями Git, рассмотрим практические примеры и закрепим полученные знания в самостоятельной работе. В конце темы мы поговорим о правилах работы над дипломным проектом.
# Урок 1: Вспоминаем Git
# Урок 1. Вспоминаем Git
Что такое Git, его базовые понятия и работу с GitHub мы разбирали на уроках [3 спринта](https://practicum.yandex.ru/trainer/ios-developer/lesson/e85a6187-9497-490f-884e-7684bce6bd07/). В этом уроке мы закрепим полученные знания и поговорим о пользе Git для командной работы.
Что такое Git, его базовые понятия и работу с GitHub мы разбирали в уроках модуля «Вёрстка, сеть и хранение данных». В этом уроке мы закрепим полученные знания и поговорим о пользе Git для командной работы.
# Проверим изученное
@@ -89,7 +84,7 @@ Git успешно справляется с этими и другими зад
- [ ] Запрос на изменение кода
**Пояснение: Нет. Ветка не является запросом, а последовательность коммитов.**
**Пояснение: Нет. Ветка — это не запрос, а последовательность коммитов.**
- [ ] То, что получается после выполнения Fork
@@ -100,11 +95,11 @@ Git успешно справляется с этими и другими зад
- [ ] Операция скачивания репозитория
**Пояснение: Нет. Pull Request относится к ветке, а не репозиторию.**
**Пояснение: Нет. Pull Request относится к ветке, а не к репозиторию.**
- [ ] Операция загрузки коммитов в удаленный репозиторий
- [ ] Операция загрузки коммитов в удалённый репозиторий
**Пояснение: Нет. Эта операция выполняется командой push.**
**Пояснение: Нет. Эта операция выполняется командой Push.**
- [x] Предложение изменений для ветки или проекта
@@ -112,79 +107,70 @@ Git успешно справляется с этими и другими зад
- [ ] Операция скачивания ветки с удалённого репозитория в локальный
**Пояснение: Нет. Pull Request относится к ветке, а не репозиторию.**
**Пояснение: Нет. Pull Request относится к ветке, а не к репозиторию.**
**КВИЗ - КОНЕЦ**
Вы освежили знания о Git из предыдущих уроков. Теперь разберём, как Git помогает при командной разработке.
Вы освежили знания о Git из предыдущих уроков. Теперь разберём, как Git помогает при командной разработке.
Командная разработка предполагает возможность одновременного изменения кода несколькими разработчиками, каждый из которых работает над своей задачей. При этом в процессе работы разработчики могут одновременно вносить изменения в один и тот же фрагмент кода.
Скажем, Иван меняет экран профиля пользователя в соответствии с макетом нового дизайна. Он делает это в отдельной ветке своей задачи. В то же время Мария работает над функционалом смены пароля пользователя и тоже вносит изменения в экран профиля.
**Кнопка-вопрос:** Как Git справляется с этим?
# Кнопка-вопрос: Как Git справляется с этим?
Скажем, Иван меняет экран профиля пользователя в соответствии с макетом нового дизайна. Он делает это в отдельной ветке своей задачи. В то же время Мария работает над функционалом смены пароля пользователя и тоже вносит изменения в экран профиля.
Git позволяет согласовывать эти изменения между собой и контролировать их.
Git позволяет согласовывать эти изменения между собой и контролировать их.
Он предоставляет следующие возможности:
- хранение полной истории изменений,
- ветвление изменений,
- слияние изменений,
- поддержку релизного процесса {{Git Flow}}[*глоссрий*: Методика работы с Git для организации релизного процесса.]. Об этом скоро поговорим.
- поддержка релизного процесса {{Git Flow}}[mob_ios_gitflow] — об этом скоро поговорим.
Разберём эти возможности подробнее.
# Хранение полной истории изменений
Git хранит полную историю изменений каждого файла. В неё входят сведения об авторе и дате, а также комментарий с описанием цели каждого изменения. История с комментариями во время чтения кода помогает понять, что этот код делает и почему действие реализовано именно таким образом. С полной историей можно легко вернуться к предыдущим версиям для анализа причин возникновения ошибок.
Git хранит полную историю изменений каждого файла. В неё входят сведения об авторе и дате, а также комментарий с описанием цели каждого изменения. История с комментариями во время чтения кода помогает понять, что этот код делает и почему действие реализовано именно таким образом. С полной историей можно легко вернуться к предыдущим версиям для анализа причин возникновения ошибок.
Например, после того как Иван и Мария завершили свои задачи, тестировщик увидел, что экран профиля не соответствует макету. Иван в этом случае может посмотреть в Git всю историю изменений экрана профиля и найти те изменения, которые вызвали несоответствие, а также дату, автора изменений и его комментарий.
Например, после того как Иван и Мария завершили свои задачи, тестировщик увидел, что экран профиля не соответствует макету. Иван в этом случае может посмотреть в Git всю историю изменений экрана профиля и найти те изменения, которые вызвали несоответствие, а также дату, автора изменений и его комментарий.
# Ветвление изменений
В примере выше Иван и Мария одновременно работали над своими задачами отдельно — каждый в своей ветке. Это позволяет отделить изменения от разных разработчиков. То есть, пока вы работаете в своей ветке, вы не влияете на код других разработчиков, а они — на ваш. С помощью Git можно легко создавать ветки для новых задач.
В примере выше Иван и Мария одновременно работали над своими задачами отдельно — каждый в своей ветке. Это позволяет разделить изменения от разных разработчиков. То есть, пока вы работаете в своей ветке, вы не влияете на код других разработчиков, а они — на ваш. С помощью Git можно легко создавать ветки для новых задач.
Ветвление также полезно, если один разработчик одновременно работает над несколькими задачами. Создание ветки под каждую задачу позволяет вести разработку задач независимо друг от друга.
Ветвление ещё и в рамках релизного процесса. Как правило, команды разработчиков создают отдельные ветки для каждого релиза.
Ветвление используется ещё и в рамках релизного процесса. Как правило, команды разработчиков создают отдельные ветки для каждого релиза.
# Слияние изменений
Когда задача Ивана готова, ему необходимо перенести свои изменения в основной код приложения. Для этого он выполняет {{слияние}}[*глоссарий: Cлияние или мёрдж (англ. merge) — объединение двух или более веток. В процессе мёрджа изменения из указанной ветки переносятся (копируются) в текущую.*] своей ветки с основной веткой проекта. Основная ветка репозитория (master или main) создаётся автоматически при создании репозитория.
Когда задача Ивана готова, ему необходимо перенести свои изменения в основной код приложения. Для этого он выполняет {{слияние}}[mob_ios_merge2] своей ветки с основной веткой проекта. Основная ветка репозитория (`master` или `main`) создаётся автоматически при создании репозитория.
Следом Мария также завершает работу и сливает свою ветку с основной веткой проекта. И здесь возможны конфликты между внесёнными ею изменениями и изменениями Ивана. Git просигнализирует об этих конфликтах и не даст выполнить слияние до тех пор, пока они не будут исправлены.
Если автоматически решить слияние конфликтующих файлов не удастся, то Git сообщит про {{мёрдж конфликт}}[*Глоссарий*: Мёрдж конфликт (merge conflict) — ситуация, когда при слиянии веток в один или несколько файлов вносились независимые изменения. В некоторых случаях (например, если изменялись разные, не пересекающиеся части одного файла)]. В таком случае необходимо самостоятельно указать, как выполнять слияние конфликтующих версий («решить конфликт», resolve merge conflict). Изменения, внесённые в процессе решения конфликта, автоматически попадают в мёрдж коммит.
Подробнее конфликты в коде и их разрешение мы разберём в следующих уроках.
Если автоматически решить слияние конфликтующих файлов не удастся, то Git сообщит про {{мерж-конфликт}}[mob_ios_mergeconflict]. В таком случае необходимо самостоятельно указать, как выполнять слияние конфликтующих версий («решить конфликт», resolve merge conflict). Изменения, внесённые в процессе решения конфликта, автоматически попадают в мерж-коммит.
Подробнее конфликты в коде и их разрешение мы разберём в следующих уроках.
# Поддержка релизного процесса Git Flow
Git Flow — это методика работы с Git для организации релизного процесса. В ней определяется, какие виды веток необходимы проекту и как выполнять слияние между ними.
Git Flow — это методика работы с Git для организации релизного процесса. В ней определяется, какие виды веток необходимы проекту и как выполнять слияние между ними.
В описании процесса Git Flow приведены следующие ветки:
- главная ветка Main хранит официальную историю релиза. В ветке Main рекомендуется присваивать всем коммитам номер версии.
- Главная ветка Main хранит официальную историю релиза. В ветке Main рекомендуется присваивать всем коммитам номер версии.
- Ветка разработки Develop предназначена для объединения всех функций.
- Под каждую новую функцию приложения нужно выделить собственную ветку Feature. Её делают на основе Develop, и она сливается с Develop по завершении работы.
- Когда стартует релиз, от ветки Develop создаётся ветка Release. Когда подготовка к выпуску релиза завершается, ветка Release сливается с Main. Eй присваивается номер версии. Кроме того, нужно выполнить её слияние с веткой Develop, в которой с момента создания ветки релиза могли возникнуть изменения.
- ветка разработки Develop предназначена для объединения всех функций.
- под каждую новую функцию приложения нужно выделить собственную ветку Feature. Её делают на основе Develop и сливается с Develop по завершении работы.
- когда стартует релиз, от ветки Develop создаётся ветка Release. Когда подготовка к выпуску релиза завершается, ветка Release сливается с Main. Eй присваивается номер версии. Кроме того, нужно выполнить её слияние с веткой Develop, в которой с момента создания ветки релиза могли возникнуть изменения.
[](/images/git-flow.png)
![](https://pictures.s3.yandex.net:443/resources/Git-flow_1683797404.png)
В ходе работы над дипломным проектом вы будете работать по схожей, но упрощённой схеме.
# Подведём итоги
В этом уроке мы вспомнили, как работать в системе управления версиями Git; поговорили про её возможности для командной разработки. Мы рассмотрели релизный процесс по методике Git Flow. В следующих уроках мы продолжим знакомство с Git на практических примерах.
В этом уроке мы вспомнили, как работать в системе управления версиями Git, поговорили про её возможности для командной разработки. Мы рассмотрели релизный процесс по методике Git Flow. В следующих уроках мы продолжим знакомство с Git на практических примерах.
# Полезная ссылка
@@ -0,0 +1,159 @@
# Sourcetree и FileMerge
В Git нет графического интерфейса. Система контроля версий предполагает работу из командной строки. Чтобы сделать работу с объектами Git (коммитами, ветками и репозиториями) более удобной, а представление этих объектов более наглядным, появились графические оболочки к Git. Например, [Sourcetree](https://www.sourcetreeapp.com/) от компании Atlassian и [GitKraken](https://www.gitkraken.com/).
При инициализации второго проекта мы рекомендовали использовать Sourcetree — продолжим работать с этой программой. Если по какой-то причине она у вас не установлена, вернитесь в урок «Что такое .gitignore» темы «Инициализация второго проекта», там есть нужная информация об установке.
В этом уроке мы познакомимся с Sourcetree поближе, а также освоим FileMerge — ещё один инструмент, работающий с Git. С их помощью мы разберём практические примеры работы с ветвлением, слиянием веток и разрешением конфликтов в коде.
**КНОПКА**
Попробуем Sourcetree в деле!
Сейчас мы создадим пустой репозиторий и добавим в него файл. Затем поработаем с ветвлением, слиянием веток и решением конфликтов.
Новый пустой репозиторий создаём так:
1. В меню Sourcetree выбираем File → New... → Create local repository.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree4_1683798341.png)
2. Справа от поля Destination path нажмите кнопку с тремя точками. Выберите путь к новому репозиторию, нажмите кнопку New Folder и создайте папку `GitTest`. Далее нажмите кнопки Open и Create.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree5_1683798367.png)
Новый репозиторий создан! Теперь добавим файл в папку `GitTest`.
3. Запустите Xcode, выберите меню File → New → File → Swift File и нажмите кнопку Next. В открывшемся окне дайте файлу имя `Number.swift`, выберите созданную для репозитория папку `GitTest` для сохранения файла и нажмите кнопку Create.
4. В открывшемся в Xcode окне редактирования файла `Number.swift` удалите всё содержимое файла и добавьте следующую строку:
```swift
let number = 111
```
Сохраните файл.
5. Перейдите снова в программу Sourcetree. Окно программы должно выглядеть вот так:
![](https://pictures.s3.yandex.net:443/resources/Sourcetree7_1683798449.png)
6. Создадим наш первый коммит в Sourcetree. Для этого поставьте галочку рядом с именем файла `Number.swift` или заголовком Unstaged files. Тем самым мы переводим наш файл в область файлов, готовых к сохранению в коммите: Staging Area. Теперь нужно ввести комментарий к коммиту. Нажмите кнопку Commit на верхней панели кнопок или поставьте курсор в поле Commit message в самом низу экрана. Введите текст комментария: «Начальное значение числа — 111» — и нажмите кнопку Commit.
В левой панели Sourcetree нажмите на кнопку History. После вы увидите вот такое окно:
![](https://pictures.s3.yandex.net:443/resources/Sourcetree9_1683798479.png)
Репозиторий и файл созданы. Файл `Number.swift` добавлен с помощью коммита в репозиторий.
Теперь мы будем работать с изменениями кода, используя Xcode и Sourcetree. Чтобы была понятна цель дальнейших шагов, представьте, что вам предстоит работать сначала над задачей `Feature222`, в которой вам придётся редактировать код, а затем — над задачей `Feature333`, где тоже требуется вносить изменения в код. Для наглядности в нашем примере мы будем редактировать только значение константы `number`. Эта константа может означать, например, максимальное количество символов в текстовом поле или высоту ячейки таблицы.
**КНОПКА**
Продолжаем!
7. Теперь создадим новую ветку от текущей ветки `master`. Нажимаем кнопку Branch на верхней панели кнопок Sourcetree. В открывшемся окне в поле New Branch вводим `Feature222` и нажимаем кнопку Create Branch. Так мы создали ветку `Feature222` от текущей ветки `master` и переключились на неё.
Обратите внимание, что теперь перед описанием коммита видны названия веток `Feature222` и `master`. Это означает, что пока ветки не различаются и наш коммит в них — последний.
8. На этом шаге мы внесём изменение в файл. Переключитесь в Xcode и отредактируйте файл `Number.swift`. Замените «111» на «222» и сохраните файл. Перейдите в Sourcetree и выберите в центральной верхней панели строку Uncommitted changes. Вы увидите следующий экран:
![](https://pictures.s3.yandex.net:443/resources/Sourcetree13_1683798564.png)
9. Поставьте флажок возле Unstaged files, как вы уже делали раньше. Нажмите кнопку Commit, введите комментарий «Значение числа — 222» и сохраните коммит. Теперь история коммитов выглядит так:
![](https://pictures.s3.yandex.net:443/resources/Sourcetree14_1683798592.png)
10. Теперь начнём работу над задачей `Feature333`. Сначала переключимся на ветку `master`, выполним её {{чекаут}}[mob_ios_чекаут].
Для этого в левой панели Sourcetree раскроем раздел Branches, выберем `master` и вызовем контекстное меню для `master`. Далее выберем Checkout master.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree15_1683798626.png)
11. Создадим ветку `Feature333` от `master`, как в шаге 7.
12. Переключимся в Xcode и отредактируем наш файл, заменив «111» на «333». Сохраним файл.
13. В Sourcetree выполним коммит с комментарием «Значение числа — 333», как мы это делали ранее. В истории коммитов Sourcetree теперь три коммита — в трёх ветках.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree17_1683798734.png)
Итак, мы завершили разработку двух мини-задач (`Feature222` и `Feature333`) в одноимённых ветках. Теперь пора провести слияние (merge) этих веток с веткой `master`. Начнём с `Feature222`.
14. Сначала выполним чекаут ветки, *в которую* будем заливать изменения. В нашем случае это ветка `master`. В разделе Branches на левой панели Sourcetree выбираем `master` → Checkout master. Команда `checkout` приводит содержимое файлов в папке `GitTest` в соответствие истории изменений из ветки `master`, а саму ветку делает активной.
Затем в том же разделе Branches выбираем контекстное меню для `Feature222` и выбираем `Merge Feature222 into master`. В появившемся окне Confirm Merge подтверждаем действие.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree19_1683798779.png)
Обратите внимание, что история коммитов изменилась и выглядит так:
![](https://pictures.s3.yandex.net:443/resources/Sourcetree20_1683798804.png)
15. Теперь зальём ветку `Feature333` в `master`. В разделе Branches выбираем контекстное меню для `Feature333` и нажимаем `Merge Feature333 into master`. В появившемся окне Confirm Merge подтверждаем действие. На этом шаге Sourcetree показывает нам, что при слиянии ветки возник конфликт.
**Почему возник конфликт:**
1. Сначала мы ответвились от ветки `master` в ветку `Feature222` и поменяли фрагмент кода.
2. Затем в рамках задачи `Feature333` мы также ответвились от `master` и изменили тот же самый фрагмент кода.
3. Затем мы добавили изменения `Feature222` в ветку `master`.
4. Конфликта не произошло, потому что Git проверил, что старое значение «111» в ветке `Feature222` изменилось на «222».
5. Иная ситуация возникла при слиянии ветки `Feature333` в `master`. Git знает, что в `Feature333` исходное значение «111» меняется на «333». Однако при слиянии Git в ветке `master` обнаруживает исходное значение «222»! Возникает конфликт между актуальным кодом в ветке `master` и тем кодом, который ожидает Git для внесения изменений.
6. Самостоятельно менять код в случае конфликта Git не может, поэтому конфликты нужно решать в ручном режиме. То есть конфликты в случае слияния веток возникают, когда Git встречает несовместимые между собой изменения, которые невозможно наложить друг на друга автоматически.
**КНОПКА**
Как увидеть конфликт в Sourcetree?
В истории коммитов перейдём в раздел Uncommitted changes и выберем наш файл `Number.swift`.
- Иконка в виде треугольника с восклицательным знаком слева от имени файла в Sourcetree показывает, что в файле есть конфликты. Правая нижняя панель отображает его содержимое.
- В случае конфликта фрагмент кода, содержащий конфликт, отображается особым образом. В нашем случае так:
```swift
<<<<<<< HEAD
let number = 222
=======
let number = 333
>>>>>>> Feature333
```
- Мы видим здесь фрагмент кода, который Git отображает в виде двух версий с помощью маркеров `<<<`, `===` и `>>>`. Верхняя версия (*над* маркером `===`) отображает код в активной ветке. В нашем случае это `master`.
- `HEAD` — указатель на текущий коммит в активной ветке, то есть на состояние, в котором репозиторий находится в данный момент. Нижняя версия (*под* маркером `===`) — это код в ветке, из которой мы берём изменения. В нашем случае это `Feature333`. Теперь нам предстоит решить этот конфликт.
**Кнопка**
Как решить конфликт?
Решать конфликты можно с помощью разных программ. Можно исправлять исходный файл в Xcode. Тогда всю работу придётся делать вручную, но зато вы получите максимальную гибкость. Например, вы сможете брать части кода из обеих конфликтующих версий и объединять их в единую версию.
Можно воспользоваться Sourcetree. Это позволит быстро решать простые конфликты, выбирая только одну из конфликтующих версий. А ещё можно из Sourcetree запускать разные программы, с помощью которых можно сравнивать два различных файла и исправлять в них конфликты. Такой вариант сочетает удобство и гибкость при исправлении конфликтов. Редактировать код в Xcode вы умеете, поэтому рассмотрим два других способа.
## Sourcetree
В меню Sourcetree пройдите по пути Actions → Resolve conflicts. В раскрывшемся контекстном меню есть два варианта:
- Resolve using 'Mine': в этом случае будет принята версия активной ветки, *в которую* заливаем изменения (`master`).
- Resolve using 'Theirs': в этом случае будет принята версия ветки, *из которой* заливаем изменения (`Feature333`).
![](https://pictures.s3.yandex.net:443/resources/Sourcetree25_1683799073.png)
После выбора одного из этих пунктов конфликт решён. Осталось только сделать коммит.
Этот способ прост и понятен, но его возможности ограничены. Он позволяет всего лишь выбрать одну версию из двух, но не позволяет их комбинировать. Рассмотрим вариант с внешней программой. В ней будет больше возможностей.
## FileMerge
В меню Sourcetree выберите Actions → Resolve conflicts. В раскрывшемся контекстном меню нужно выбрать Launch External Merge Tool. Этот пункт меню запускает внешнюю программу для слияния изменений — FileMerge.
![](https://pictures.s3.yandex.net:443/resources/Sourcetree28_1683799103.png)
Окно FileMerge показывает слева версию кода в текущей ветке (LOCAL), а справа — в той, из которой мы заливаем изменения (REMOTE). В нижней части окна показывается итоговый вариант кода.
Список действий (Actions) в правом нижнем углу позволяет выполнить пять действий:
- Choose left — выбрать версию слева;
- Choose right — выбрать версию справа;
- Choose both (left first) — выбрать обе (сначала левая);
- Choose both (right first) — выбрать обе (сначала правая);
- Choose neither — не выбирать никакую.
Переключитесь последовательно между ними и посмотрите, как меняется итоговый код в нижней части экрана. Затем выберите Choose right (версия из `Feature333`). Затем в меню FileMerge выберите File → Save Merge → Close. После этого в Sourcetree в разделе Unstaged files пометьте файл `Number.swift` и выполните Commit. Файл (или файлы), оставшиеся от работы FileMerge (например, `Number.swift.orig`), можно удалить. Для этого нажмите на три точки справа от файла и выберите Remove File.
В этом примере мы рассмотрели программу FileMerge. Но вы можете настроить Sourcetree на использование другой программы. Для этого заходите в меню Sourcetree → Settings → Diff и выбирайте нужную программу из списка Merge Tool. До этого программа должна уже быть установлена на вашем компьютере.
# Подведём итоги
В этом уроке мы на практике рассмотрели приёмы работы с Git для работы в команде. Мы использовали программу Sourcetree. Если вы следовали инструкции, то научились создавать ветки для реализации задач, выполнять слияние веток и решать возникающие в коде конфликты с помощью программ Sourcetree и FileMerge. В следующем уроке мы продолжим практику решения конфликтов на более сложных примерах.
@@ -0,0 +1,151 @@
# Разрешение конфликтов
В прошлом уроке мы рассмотрели, как в программном коде возникают конфликты. Мы разобрали небольшой практический пример решения конфликта с помощью Sourcetree и FileMerge.
Поскольку конфликты в коде при командной разработке возникают регулярно и вам придётся их решать, теперь разберём анатомию конфликта, а затем потренируемся на ещё одном практическом примере.
В прошлом уроке вы могли заметить, что в одном случае при слиянии ветки с веткой `master` конфликта не было, а в другом он возник.
**КНОПКА**
Почему так происходит?
Рассмотрим пример на иллюстрации:
![](https://pictures.s3.yandex.net:443/resources/Merge1_1683799302.png)
Здесь ветка Some Feature была создана от ветки Main. После этого в Some Feature было добавлено два коммита, а Main осталась неизменной.
![](https://pictures.s3.yandex.net:443/resources/Merge2_1683799330.png)
При слиянии Some Feature и Main Git проверяет историю коммитов и видит, что:
- ветка Main не изменялась;
- для слияния достаточно перевести указатель последнего коммита ветки Main на последний коммит в ветке Some Feature.
Это вариант слияния без конфликтов, который называется *«слияние перемоткой»* (англ. fast-forward merge). Здесь Git не меняет историю коммитов, а просто передвигает указатель ветки Main.
Как и в прошлом примере, после создания ветки Some Feature в неё были добавлены два коммита. Но в ветку Main после этого был добавлен один коммит.
![](https://pictures.s3.yandex.net:443/resources/Merge3_1683799386.png)
В этой ситуации при слиянии веток должен появиться коммит c двумя родительскими коммитами:
- последний коммит ветки Main;
- последний коммит ветки Some Feature.
![](https://pictures.s3.yandex.net:443/resources/Merge4_1683799409.png)
Это мерж-коммит (merge commit) — автоматический коммит по завершении процесса слияния веток. Мерж-коммит содержит в себе все изменения ветки мержа при объединении с текущей веткой, начиная с последнего общего коммита.
При таком слиянии Git вынужден проводить сравнение сразу *трёх* коммитов:
- последний коммит ветки Main;
- последний коммит ветки Some Feature;
- последний общий коммит обеих веток, начиная с которого история коммитов в ветках различается.
Такой вариант слияния коммитов называется *«трёхстороннее слияние»* (3-way merge). Обработав историю коммитов обеих веток с общего коммита-предка, Git может обнаружить конфликты, которые он не может исправить самостоятельно. Именно в этом случае подключается разработчик.
А теперь вернёмся к практике.
**КНОПКА**
Продолжаем!
Для дальнейшей работы скачайте архив проекта и распакуйте его.
**НОВАЯ ЭКШН-КНОПКА С ОБЯЗАТЕЛЬНЫМ НАЖАТИЕМ: [К файлу]([GitExample](/gitexample.zip))**
Далее откройте проект в Xcode, а репозиторий проекта — в Sourcetree.
Проект вам уже знаком: это иллюстрация паттерна MVVM на замыканиях. В проекте три ветки: `main`, `Feature1` и `Feature2`. Последние две ответвлены от начального коммита `main`. Для практики работы с конфликтами в проект были внесены следующие изменения:
1. В ветке `main` добавлен файл `EmptyFile.swift`.
2. В ветке `Feature1`:
- изменён фон `LaunchScreen`,
- удалён файл `EmptyFile.swift`,
- в `SuccessViewController.swift` изменены emoji и его размер, а также порядок вызова функций `setupUI()` и `setupLayout()`.
3. В ветке `Feature2`:
- изменён фон `LaunchScreen`,
- `EmptyFile.swift` переименован в `VoidFile.swift`,
- в `SuccessViewController.swift` изменён emoji и его размер.
Ветка `Feature1` уже залита в `main`, а ветку `Feature2` будем заливать сейчас.
**КНОПКА-ВОПРОС** Будем решать конфликты?
Да, нам придётся это сделать, так как изменения в `Feature1` и `Feature2` противоречат друг другу.
1. Начнём с чекаута ветки `main` в Sourcetree. Затем, выбрав ветку `Feature2`, выполним команду `Merge Feature2 into main`.
2. На экранах Confirm merge и Merge conflicts подтвердим действие.
3. В истории коммитов выберем Uncommitted changes. Экран будет выглядеть примерно так:
![](https://pictures.s3.yandex.net:443/resources/03_Sourcetree2_1683799635.png)
4. Теперь обратимся к конфликту в файле `LaunchScreen.storyboard`: в нём был изменён только цвет фона. Выделим этот файл в разделе Staged files, а далее выберем в меню Actions → Resolve Conflicts → Launch External Merge Tool. В результате увидим вот что:
![](https://pictures.s3.yandex.net:443/resources/03_Sourcetree3_1683799663.png)
Файлы `storyboard` содержат данные в формате XML. Такой формат нелегко анализировать — в нём непросто исправлять конфликты:
- У него иерархическая структура.
- Один тег может содержать сразу несколько атрибутов. Например, для `color` их сразу шесть!
Если вы хотите облегчить себе работу по исправлению конфликтов при работе над дипломным проектом, то выполняйте вёрстку в коде.
5. Для решения конфликта выберем версию `LOCAL` в ветке `main`, то есть версию кода в *левом окне*. `LOCAL` можно увидеть в названии файла `LaunchScreen_LOCAL` в верхней части окна. Нажмём на каждую из стрелок (1 и 2) в середине окна и для каждой в списке Actions внизу справа выберем Choose left. После этого пройдём в меню File → Save Merge и закроем окно FileMerge.
> Совет: не забывайте исправлять все конфликты в файле. Их количество FileMerge показывает в нижнем левом углу окна.
6. Дальше нам нужно разобраться с файлом проекта `project.pbxproj`. Но прежде переключимся в Xcode и посмотрим на проект: проект и его файлы не отображаются в Xcode. Дело в том, что если структура файла проекта нарушена, то Xcode не может его отобразить и не позволяет работать с проектом. Важно помнить, что если допустить ошибку при решении конфликтов в файле проекта, то мы не сможем дальше с ним работать!
> Совет: если вы всё-таки допустили ошибку во время мержа и хотите начать сначала, выполните в терминале команду `git merge --abort`. Терминал проще запустить, если в Finder выбрать папку проекта, открыть для неё контекстное меню и выбрать New Terminal at Folder.
Теперь переключимся в Sourcetree. Выберем в Staged files файл проекта `project.pbxproj` и запустим FileMerge.
![](https://pictures.s3.yandex.net:443/resources/03_Sourcetree4_1_1683799809.png)
В левой нижней части окна видим, что в файле четыре конфликта из-за файла `VoidFile.swift`. Удалим ссылку на него из файла проекта. Для этого исправим конфликты, выбирая стрелки от 1 до 4. Для каждой в Actions выберем Choose left, то есть версию, в которой файл в проекте отсутствует. После этого пройдём в меню File → Save Merge и закроем окно FileMerge.
7. Мы удалили ссылку на файл `VoidFile.swift` в файле проекта. Но теперь нам нужно удалить и сам файл. Найдём его в Staged files, нажмём на три точки справа от названия файла и выберем Remove file.
![](https://pictures.s3.yandex.net:443/resources/03_Sourcetree5_1683800004.png)
В открывшемся окне с вопросом «Confirm?» подтверждаем действие.
8. На этом шаге исправим `SuccessViewController`. FileMerge показывает три конфликта.
**Задание** Исправьте конфликты самостоятельно так, чтобы метод `viewDidLoad()` выглядел вот так:
```swift
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
setupUI()
}
```
А метод `setupUI()` — так:
```swift
private func setupUI() {
view.backgroundColor = .white
successLabel.text = "🌞"
successLabel.font = .systemFont(ofSize: 110)
}
```
**КНОПКА: Сделано**
После этого выполним команду Save Merge в FileMerge и закроем FileMerge.
9. В разделе Unstaged files поставим флажки рядом с файлами `project.pbxproj` и `LaunchScreen.storyboard`. Раздел Staged files останется пустым. Выполним коммит, оставив комментарий по умолчанию. После этого удалим временные файлы `*.orig` в разделе Unstaged files.
10. Переходим в Xcode. Убедимся, что:
- в `LaunchScreen.storyboard` цвет фона для View — `System Gray 2 Color`;
- в проекте нет файлов `EmptyFile.swift` или `VoidFile.swift`;
- код в `SuccessViewController` соответствует коду в шаге 8.
Наконец, скомпилируем проект. Если он компилируется без ошибок, то слияние веток выполнено правильно. Если нет, вероятно, где-то остались маркеры конфликта от Git: `<<<`, `===` и `>>>`. Нужно их найти и исправить.
# Подведём итоги
В этом уроке мы вместе разобрали две стратегии Git при слиянии веток: слияние перемоткой (fast-forward merge) и трёхстороннее слияние (3-way merge). Вы попрактиковались в решении конфликтов разных типов: в коде, файлах Storyboard, файле проекта. Вы также узнали о неудобствах и рисках, связанных с исправлением конфликтов в файлах Storyboard и файле проекта. В следующем уроке посмотрим, как работать над дипломным проектом.
@@ -0,0 +1,73 @@
# Работа над проектом
Над дипломным проектом вы будете работать в команде — порядок работы над задачами в следующих спринтах изменится. В уроке мы поговорим о правилах и {{лучших практиках}}[mob_ios_best_practices], которые помогут избежать ошибок в командной работе.
Для вас будет подготовлен базовый код проекта — до начала вашей работы.
1. Создайте форк репозитория для своей команды. Договоритесь, кто из вас это сделает.
2. Сделайте в нём ветки `main`, `develop` и `release`.
3. После распределения эпиков каждый из вас начнёт разработку задач своего эпика.
## Правило № 1: работа с ветками
*Не делайте коммиты в ветки `main`, `develop` и `release`! Работайте только в ветке своего эпика или задачи.*
> Мы хотим, чтобы каждый из вас разработал примерно одинаковый набор функциональности и не попал в ситуацию, когда задачи и сроки сильно зависят от результатов другого человека: кто-то из студентов всё ещё может выпасть из командной работы. С таким подходом мы можем проводить ревью более прозрачно.
Вам предстоит создавать собственные ветки от ветки `develop` под ваш эпик. Название ветки должно быть кратким и ёмким, чтобы можно было понять суть задачи в целом. Например, если делаете задачу по вёрстке экрана профиля, можно назвать ветку `Profile_Screen_UI`. Завершив работу над эпиком в своей ветке, вы создадите Pull Request в ветку `develop` и пригласите наставника и коллег провести ревью вашего кода.
## Правило № 2: работа с коммитами
*Старайтесь делать коммиты после каждой небольшой доработки вашей задачи.*
Так ваш Pull Request будет проще анализировать. Коммит — это решение мини-задачи в рамках большой задачи. Например, добавление нового сетевого запроса или обработки нажатия кнопки. Старайтесь писать информативные комментарии к коммитам, чтобы можно было понять их цель. Например, просто слово fix — это плохой вариант комментария. А вот Fix background color или «Исправление цвета фона» — более информативный комментарий.
## Совет по работе с коммитами
*Каждый коммит лучше заливать сразу в удалённый репозиторий. Никто не застрахован от сбоев в работе компьютера. Чтобы случайно не потерять все наработки, синхронизируйте вашу локальную ветку с удалённым репозиторием.*
Если вы делаете коммит в Xcode через меню Source Control → Commit, установите флажок Push to remote в левом нижнем углу окна. В Sourcetree для этой же цели используйте кнопку Push на панели кнопок.
## Правило № 3: работа с Pull Request
Ревьюеры будут отсматривать ваши пул-реквесты. Если у них будут замечания к коду или другим вещам — полностью отрабатывайте их, и тогда ревьюверы одобрят Pull Request.
Ревью не будет односторонним: другие студенты будут приглашать вас отсмотреть их работы.
Когда вы закончите задачи и ревью будут пройдены, залейте содержимое вашего Pull Request в ветку `develop`. Могут возникнуть конфликты, но вы узнали в предыдущих уроках, как их решать.
## Правило № 4: проверка собираемости проекта
Когда вы исправили замечания, полученные в ходе ревью кода, и конфликты, на наличие которых указывает статус вашего Pull Request, *необходимо проверить, что проект собирается*. Это нужно сделать до того, как вы залили Pull Request.
*Кроме того, убедитесь, что ваша реализация строго соответствует требованиям, изложенным в ТЗ.*
Если этого не сделать, то возможны ситуации, когда вы зальёте в ветку `develop` код с критическими для проекта ошибками. Другие разработчики, ответвляясь от `develop`, заберут эти ошибки в свои ветки. Это приведёт к исправлениям в нескольких ветках. Лучше исправить ошибку один раз в своей ветке. Кроме того, изначальная ошибка может породить дополнительные конфликты.
## Правило № 5: актуальность ветки develop
*Регулярно обновляйте вашу локальную ветку `develop` из удалённого репозитория и подмерживайте её в свою рабочую ветку. Это поможет избежать больших и сложных конфликтов при слиянии вашего эпика в `develop`.*
Это поможет свести конфликты в коде к минимуму. Прежде чем ответвиться от `develop`, выполните команду `pull` для ветки `develop`. Например, вы закончили свою первую задачу и берёте вторую. В вашем локальном репозитории уже есть ветка `develop`, но, возможно, она уже устарела. Выполните команду `pull` для ветки `develop`, получив свежую копию из центрального репозитория.
## Правило № 6: использование .gitignore
*Используйте `.gitignore` для исключения файлов проекта, которые не должны попадать в коммиты.*
Это касается использования подов, временных и отладочных файлов, файлов с личными ключами и конфигурациями — всего, что не должно храниться в истории кода проекта.
## Подведём итоги
Мы разобрали фазы работы над продуктом и роли разработчиков и разработчиц, описали порядок работы над дипломным проектом. Затем вы поработали с изменениями в коде и потренировались решать конфликты с Sourcetree и FileMerge, узнали правила командной разработки. Всё это пригодится вам в следующих спринтах.
Сверимся с картой курса. Вы здесь:
![](https://pictures.s3.yandex.net:443/resources/Git._Rabota_nad_proektom_1697034669.png)
Верим, что у вас всё получится! Готовы идти дальше?
КНОПКА Да!
В этом коротком спринте вы подготовились к ведению разработки в команде. Мы разобрали фазы работы над продуктом и роль разработчика в каждой из них. После этого мы описали порядок работы над дипломным проектом. Затем мы поработали с изменениями в коде и потренировались в разрешении конфликтов в нём с помощью Sourcetree и FileMerge. А в конце мы разобрали несколько важных правил для работы над дипломным проектом.
Начиная со следующего спринта всё это поможет при работе над проектом. Успехов!

Before

Width:  |  Height:  |  Size: 555 KiB

After

Width:  |  Height:  |  Size: 555 KiB

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 713 KiB

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before

Width:  |  Height:  |  Size: 430 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Before

Width:  |  Height:  |  Size: 481 KiB

After

Width:  |  Height:  |  Size: 481 KiB

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 443 KiB

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 286 KiB

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Before

Width:  |  Height:  |  Size: 667 KiB

After

Width:  |  Height:  |  Size: 667 KiB

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before

Width:  |  Height:  |  Size: 485 KiB

After

Width:  |  Height:  |  Size: 485 KiB

Before

Width:  |  Height:  |  Size: 726 KiB

After

Width:  |  Height:  |  Size: 726 KiB

@@ -0,0 +1,46 @@
# Сдаём задачу спринта 25 на ревью — 1/3 эпика
> Срок проверки этого задания и доработок по нему составит не более 48 часов.
В 25-м спринте ревью состоит из двух итераций:
1. Вам нужно сдать проект, в котором реализована 1/3 часть эпика согласно вашей декомпозиции, внести правки по комментариям ревьюера и сдать работу на повторное ревью. Эту итерацию вы сдаёте в окошко к этому уроку.
2. Вам нужно реализовать 2/3 части эпика согласно вашей декомпозиции, пройти код-ревью внутри команды и сдать 2/3 вашего эпика на ревью. Эту итерацию вы сдаёте в окошко следующего урока.
Напоминаем, что все три части эпика должны быть равноценными и примерно одинаковыми по объёму.
Вы прошли уже очень много. Как всегда: верим, что у вас всё получится!
![](https://pictures.s3.yandex.net:443/resources/Sdaiom_zadachu_sprinta_19_na_reviu_1697034739.png)
## Убедитесь, что
1. В вашем Pull Request приложена ссылка на таск-трекер. В доске должна быть информация о вашем прогрессе, в том числе:
- выбранная командой архитектура *(согласно ТЗ: MVVM)* и способ вёрстки *(согласно ТЗ: SwiftUI)*;
- соблюдение ключевых нефункциональных требований *(согласно ТЗ: минимальная версия iOS 17.0, async/await для работы с сетью и многопоточностью и т. д.)*;
- декомпозиция эпика *(ссылка на таск-трекер)*;
- декомпозиция эпика *(приложенный текстовый документ `.md`)*;
- первоначальная оценка времени выполнения задач;
- реальное время, потраченное на решение каждой задачи.
Обратите внимание на последний пункт — потраченное время. Это важно для улучшения ваших навыков декомпозиции. Сравнивая первоначальную оценку с реальным временем, вы можете отследить, насколько реалистично оцениваете свои силы и полностью ли предусмотрели шаги в задаче. Зная это, в следующий раз вы сможете дать более точный прогноз.
2. Каждая итерация выполнена в новой ветке от ветки вашего эпика.
![](https://pictures.s3.yandex.net:443/resources/diplomnyi_proekt__skhema_vetok_2_1694690814.png)
3. В первой итерации сдачи задания вы реализовали 1/3 задач своего эпика, а во второй итерации — 2/3 задач своего эпика. По вашему таск-трекеру понятно, как вы разделили декомпозицию на три части, какие именно задачи сделаны, а какие всё ещё остались в бэклоге.
4. Во второй итерации сдачи работы вы провели код-ревью с партнёром из вашей команды, оставили друг другу комментарии и обработали их.
5. Сделанная вами работа соответствует базовому варианту из таблицы с критериями:
- [Критерии ревью индивидуального PR, если вы выбрали UIKit.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
- [Критерии ревью индивидуального PR, если вы выбрали SwiftUI.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
## Сроки и доработки
Ваше задание проверит ревьюер и оставит подробные комментарии. Ожидайте письмо об окончании ревью на почту, с которой вы зарегистрировались в Практикуме. Туда придёт уведомление, что ревьюер оставил вам обратную связь. Чтобы прочитать отзыв, снова зайдите в специальную форму на сайте Практикума.
После возврата задания **первой итерации** на доработку у вас будет одна неделя для внесения правок и работы над следующей частью эпика.
После возврата задания **второй итерации** на доработку у вас будет одна неделя следующего спринта для внесения правок и завершения работы над эпиком.
![](https://pictures.s3.yandex.net:443/resources/image_2_1685040736.png)
@@ -0,0 +1,46 @@
# Сдаём задачу спринта 25 на ревью — 2/3 эпика
> Срок проверки этого задания и доработок по нему составит не более 48 часов.
В 25-м спринте ревью состоит из двух итераций:
1. Вам нужно сдать проект, в котором реализована 1/3 часть эпика согласно вашей декомпозиции, внести правки по комментариям ревьюера и сдать работу на повторное ревью. Эту итерацию вы сдаёте в окошко к предыдущему уроку.
2. Вам нужно реализовать 2/3 части эпика согласно вашей декомпозиции, пройти код-ревью внутри команды и сдать 2/3 вашего эпика на ревью. Эту итерацию вы сдаёте в окошко этого урока.
Напоминаем, что все три части эпика должны быть равноценными и примерно одинаковыми по объёму.
Вы прошли уже очень много. Как всегда: верим, что у вас всё получится!
![](https://pictures.s3.yandex.net:443/resources/Sdaiom_zadachu_sprinta_19_na_reviu_1697034739.png)
## Убедитесь, что
1. В вашем Pull Request приложена ссылка на таск-трекер. В доске должна быть информация о вашем прогрессе, в том числе:
- выбранная командой архитектура *(согласно ТЗ: MVVM)* и способ вёрстки *(согласно ТЗ: SwiftUI)*;
- соблюдение ключевых нефункциональных требований *(согласно ТЗ: минимальная версия iOS 17.0, async/await для работы с сетью и многопоточностью и т. д.)*;
- декомпозиция эпика *(ссылка на таск-трекер)*;
- декомпозиция эпика *(приложенный текстовый документ `.md`)*;
- первоначальная оценка времени выполнения задач;
- реальное время, потраченное на решение каждой задачи.
Обратите внимание на последний пункт — потраченное время. Это важно для улучшения ваших навыков декомпозиции. Сравнивая первоначальную оценку с реальным временем, вы можете отследить, насколько реалистично оцениваете свои силы и полностью ли предусмотрели шаги в задаче. Зная это, в следующий раз вы сможете дать более точный прогноз.
2. Каждая итерация выполнена в новой ветке от ветки вашего эпика.
![](https://pictures.s3.yandex.net:443/resources/diplomnyi_proekt__skhema_vetok_2_1694690814.png)
3. В первой итерации сдачи задания вы реализовали 1/3 задач своего эпика, а во второй итерации — 2/3 задач своего эпика. По вашему таск-трекеру понятно, как вы разделили декомпозицию на три части, какие именно задачи сделаны, а какие всё ещё остались в бэклоге.
4. Во второй итерации сдачи работы вы провели код-ревью с партнёром из вашей команды, оставили друг другу комментарии и обработали их.
5. Сделанная вами работа соответствует базовому варианту из таблицы с критериями:
- [Критерии ревью индивидуального PR, если вы выбрали UIKit.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
- [Критерии ревью индивидуального PR, если вы выбрали SwiftUI.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
## Сроки и доработки
В течение 48 часов ваше задание проверит ревьюер и оставит подробные комментарии. Ожидайте письмо об окончании ревью на почту, с которой вы зарегистрировались в Практикуме. Туда придёт уведомление, что ревьюер оставил вам обратную связь. Чтобы прочитать отзыв, снова зайдите в специальную форму на сайте Практикума.
После возврата задания **первой итерации** на доработку у вас будет одна неделя для внесения правок и работы над следующей частью эпика.
После возврата задания **второй итерации** на доработку у вас будет одна неделя следующего спринта для внесения правок и завершения работы над эпиком.
![](https://pictures.s3.yandex.net:443/resources/image_2_1685040736.png)
@@ -0,0 +1,44 @@
# Сдаём задачу спринта 26 на ревью — 3/3 эпика
> Срок проверки этого задания и доработок по нему составит не более 48 часов.
Вы завершаете финальный, 26-й спринт. Пришло время для ревью!
В этом спринте ревью состоит из двух итераций:
1. Вы сдаёте свой полностью готовый эпик. Ревьюер оценит работу по всем критериям из таблицы, оставит комментарии в вашем Pull Request. Вы вносите последние изменения по замечаниям ревьюера и получаете от него разрешение на слияние с общей веткой. Эту итерацию вы сдаёте в окошко к этому уроку.
2. Вы мержите свой эпик с коллегами по команде в общую ветку. Как только закончили — присылаете Pull Request из общей ветки в `master` на финальное ревью! Эту итерацию вы сдаёте в окошко к следующему уроку.
> 📌 Не забывайте руководствоваться **правилами из темы спринта про Git** (урок 4 «Работа над проектом»).
Если вы закончили работу над своим эпиком и результат соответствует базовой версии критериев, **вы можете выбрать одну или несколько дополнительных задач** (согласовав с другими участниками команды). К таким задачам относятся:
- локализация,
- тёмная тема,
- Яндекс Метрика,
- экран авторизации,
- экран онбординга,
- алерт с предложением оценить приложение,
- сообщение о сетевых ошибках,
- launch screen,
- поиск по таблице/коллекции в своём эпике.
При работе над дополнительными задачами руководствуйтесь теми же рекомендациями, что и при работе над основными задачами.
Некоторые дополнительные задачи будет гораздо проще сделать, если при разработке эпика вы будете сразу писать код с их учётом. Для локализации — не объявлять видимые для пользователя строки в коде, а использовать `NSLocalizedString`. Для тёмной темы — задавать цвета не константами, а с использованием каталога ассетов. Даже если вы не будете делать дополнительные задачи, рекомендуем учитывать это при разработке эпиков — это хорошая привычка, которая делает вас более ценным разработчиком.
Если вы работали над дополнительными задачами, они также должны быть влиты в мастер-ветку вашей команды.
## Убедитесь, что для первой итерации ревью
1. Вы полностью реализовали свой эпик.
2. Обработали все замечания от коллег в команде и ревьюера.
3. Сдаёте ссылку на Pull Request с вашим эпиком.
4. Сделанная вами работа соответствует базовому варианту из таблицы с критериями.
5. Исправили критические комментарии ревьюера.
6. Получили разрешение от ревьюера на слияние ветки с командной.
[Критерии ревью индивидуального PR, если вы выбрали UIKit.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
[Критерии ревью индивидуального PR, если вы выбрали SwiftUI.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
![](https://pictures.s3.yandex.net:443/resources/Untitled_Artwork_1_1685127786.png)
@@ -0,0 +1,40 @@
# Сдаём задачу спринта 26 на ревью — смерженный проект
В этом спринте ревью состоит из двух итераций:
1. Вы сдаёте свой полностью готовый эпик. Ревьюер оценит работу по всем критериям из таблицы, оставит комментарии в вашем Pull Request. Вы вносите последние изменения по замечаниям ревьюера и получаете от него разрешение на слияние с общей веткой. Эту итерацию вы сдаёте в окошко к предыдущему уроку.
2. Вы мержите свой эпик с коллегами по команде в общую ветку. Как только закончили — присылаете Pull Request из общей ветки в `master` на финальное ревью! Эту итерацию вы сдаёте в окошко к этому уроку.
> 📌 Не забывайте руководствоваться **правилами из темы 18-го спринта про Git** (урок 4 «Работа над проектом»).
Если вы закончили работу над вашим эпиком и результат соответствует базовой версии критериев, **вы можете выбрать одну или несколько дополнительных задач** (согласовав с другими участниками команды). К таким задачам относятся:
- локализация,
- тёмная тема,
- Яндекс Метрика,
- экран авторизации,
- экран онбординга,
- алерт с предложением оценить приложение,
- сообщение о сетевых ошибках,
- launch screen,
- поиск по таблице/коллекции в своём эпике.
При работе над дополнительными задачами руководствуйтесь теми же рекомендациями, что и при работе над основными задачами.
Некоторые дополнительные задачи будет гораздо проще сделать, если при разработке эпика вы будете сразу писать код с их учётом. Для локализации — не объявлять видимые для пользователя строки в коде, а использовать `NSLocalizedString`. Для тёмной темы — задавать цвета не константами, а с использованием каталога ассетов. Даже если вы не будете делать дополнительные задачи, рекомендуем учитывать это при разработке эпиков — это хорошая привычка, которая делает вас более ценным разработчиком.
Если вы работали над дополнительными задачами, они также должны быть влиты в мастер-ветку вашей команды.
Также вам нужно сделать скринкаст (запись экрана) с демонстрацией эпика и приложить ссылку к пул-реквесту. Для записи вы можете использовать, например, сервис Loom или другие инструменты.
## Убедитесь, что для второй итерации ревью
1. Вы исправили критические комментарии ревьюера и получили разрешение от ревьюера на слияние ветки с командной.
2. Вместе с командой слили все свои ветки в одну общую.
3. Записали и приложили скринкаст с демонстрацией своего эпика в Readme (рекомендуемая продолжительность — 1 минута, допустимая продолжительность — до 3 минут).
4. Конфликты при мерже решены, итоговый смерженный проект соответствует техническому заданию *(согласно ТЗ: архитектура MVVM, способ вёрстки SwiftUI, минимальная версия iOS 17.0, async/await для работы с сетью и многопоточностью)*.
[Критерии ревью индивидуального PR, если вы выбрали UIKit.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
[Критерии ревью индивидуального PR, если вы выбрали SwiftUI.](https://code.s3.yandex.net/Mobile/iOS/pdf/...)
![](https://pictures.s3.yandex.net:443/resources/Untitled_Artwork_1_1685127786.png)
@@ -36,7 +36,7 @@
Одна переменная типа `Int` может хранить одно целое число, которое не больше 2147483647 и не меньше 2147483648. Запоминать точный диапазон допустимых значений не обязательно: Swift всегда подскажет, если присваиваемое значение не соответствует указанному типу.
Для хранения переменной типа `Int` система резервирует память (даже если в переменной будет храниться число 0). Объём резерва зависит от архитектуры процессора: для iPhone 5, например, это будет 32 бита, для iPhone 7 — уже 64.
Для хранения переменной типа `Int` система резервирует память (даже если в переменной будет храниться число 0). Объём резерва зависит от архитектуры процессора: для старых моделей iPhone (до 5 включительно) и для Apple Watch (из-за ограничений памяти) это будет 32 бита, для современных моделей iPhone — уже 64.
Объявление переменной типа `Int` выглядит следующим образом:
@@ -47,6 +47,11 @@ print(twoLineString)
КНОПКА
**Продолжить реализацию алгоритма**
Сейчас мы совместим только что изученную технику работы с функциями со знаниями о том, как работать с массивами и циклами, а также научимся вызывать одни функции внутри других. Для этого нам надо:
- написать ещё две функции: **«Изучить систему»** и **«Исследовать галактику»**;
- объединить все шаги алгоритма, вызвав главную функцию **«Исследовать галактику»**.
## Функция «Изучить систему»
У функции **«Изучить систему»** три параметра:
@@ -107,11 +112,6 @@ func researchSystem(shipName: String, systemName: String, systemPlanets: [String
На 4-м шаге функции **«Изучить систему»** мы использовали объявленную ранее функцию **«Изучить планету»**. Как видите, функции можно вызывать внутри других функций! Используя эту особенность написания кода, мы можем дробить сколь угодно большие и сложные алгоритмы на множество маленьких частей. И для каждой сможем написать свою функцию, а затем соединить полученные шаги в один большой алгоритм, не расписывая всё в подробностях и избегая дублирования кода.
Сейчас мы совместим только что изученную технику работы с функциями со знаниями о том, как работать с массивами и циклами, а также научимся вызывать одни функции внутри других. Для этого нам надо:
- написать ещё две функции: **«Изучить систему»** и **«Исследовать галактику»**;
- объединить все шаги алгоритма, вызвав главную функцию **«Исследовать галактику»**.
## Функция «Исследовать галактику»
Закрепим полученный опыт, написав главную функцию для решения задачи — «Исследовать галактику». Она будет принимать три параметра:
@@ -239,6 +239,8 @@ print("️ Найдено \(foundSpeciesInGalaxy.count) форм жизни")
4. Вызываем функцию `researchGalaxy` и передаём в неё все параметры. Обратите внимание: равно как и при объявлении функции, если параметров много и они не умещаются на одну строку, нужно использовать **многострочный синтаксис вызова функции**, где на первой строке написано её название и есть открывающая скобка списка параметров `researchGalaxy(`; затем, на каждой строке — по одному параметру через запятую, а на последней строке — закрывающая скобка списка {formula}\\{/formula}параметров `)`.
5. Выводим в консоль количество элементов массива `foundSpeciesInGalaxy`.
> Переносить строки кода объявления и вызова функции не обязательно, но желательно, так как это повышает качество кода с точки зрения читаемости.
Запустите Playground и посмотрите, что вывелось в консоль.
@@ -368,13 +368,13 @@ ivan.sad() // 4
print(Person.happyPersonsCount) // 5
```
- [ ] 0
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Persont.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Persont.happyPersonsCount равняется 2) и 3 (Persont.happyPersonsCount равняется 3). Код на строке 4 уменьшает Persont.happyPersonsCount на единицу и он становится равен 2
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Person.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Person.happyPersonsCount равняется 2) и 3 (Person.happyPersonsCount равняется 3). Код на строке 4 уменьшает Person.happyPersonsCount на единицу и он становится равен 2
- [ ] 1
После кода на строке 1, мы увеличиваем Person.count на единицу (Persont.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Persont.happyPersonsCount равняется 2) и 3 (Persont.happyPersonsCount равняется 3). Код на строке 4 уменьшает Persont.happyPersonsCount на единицу и он становится равен 2
После кода на строке 1, мы увеличиваем Person.count на единицу (Person.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Person.happyPersonsCount равняется 2) и 3 (Person.happyPersonsCount равняется 3). Код на строке 4 уменьшает Person.happyPersonsCount на единицу и он становится равен 2
- [x] 2
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Persont.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Persont.happyPersonsCount равняется 2) и 3 (Persont.happyPersonsCount равняется 3). Код на строке 4 уменьшает Persont.happyPersonsCount на единицу и он становится равен 2
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Person.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Person.happyPersonsCount равняется 2) и 3 (Person.happyPersonsCount равняется 3). Код на строке 4 уменьшает Person.happyPersonsCount на единицу и он становится равен 2
- [ ] 3
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Persont.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Persont.happyPersonsCount равняется 2) и 3 (Persont.happyPersonsCount равняется 3). Код на строке 4 уменьшает Persont.happyPersonsCount на единицу и он становится равен 2
После кода на строке 1, мы увеличиваем Person.happyPersonsCount на единицу (Person.happyPersonsCount равняется 1), аналогично после выполения строки 2 (Person.happyPersonsCount равняется 2) и 3 (Person.happyPersonsCount равняется 3). Код на строке 4 уменьшает Person.happyPersonsCount на единицу и он становится равен 2
# Наблюдатели свойства (Property Observers)
@@ -144,7 +144,7 @@ $ git branch
Перед тем как начать слияние, нужно перейти в ветку, куда должны добавиться изменения. Обычно это главная ветка. Перейдите в неё и вызовите команду `git merge` с именем присоединяемой ветки `new_branch` в качестве параметра.
```bach
```bash
$ git checkout main # переключились на главную ветку
$ git merge new_branch # объединили ветки
@@ -35,7 +35,7 @@
– экран выбора города для просмотра погоды,
экран настроек.
Всё это могут быть разные экраны в `Storyboard`, и только вам решать, какой экран открыть в зависимости от действий пользователя. Такой экран называется *контроллер*.
Всё это могут быть разные экраны в `Storyboard`, и только вам решать, какой экран открыть в зависимости от действий пользователя. За жизненный цикл и внешний вид экрана отвечает *контроллер*.
> Контроллер — это объект типа `UIViewController`, который контролирует один экран. У каждого экрана — свой контроллер.
@@ -179,7 +179,7 @@ skyImageView.tintColor = .gray
**Практическое задание 2**
«Измените фоновый цвет кнопки с синего на зелёный в методе viewDidload. За цвет фона кнопки отвечает свойство `backgroundColor`. Зелёный цвет обозначается как `UIColor.green`»
«Измените фоновый цвет кнопки с синего на зелёный в методе viewDidload. За цвет фона кнопки отвечает свойство `tintColor`. Зелёный цвет обозначается как `UIColor.green`»
**Самопроверка** «Если при запуске приложения цвет кнопки — зелёный, а в `Storyboard'е` она любого другого цвета - поздравляем, вы справились!»
@@ -119,6 +119,7 @@ private func showAnswerResult(isCorrect: Bool) {
imageView.layer.masksToBounds = true // 1
imageView.layer.borderWidth = 8 // 2
imageView.layer.borderColor = isCorrect ? UIColor.ypGreen.cgColor : UIColor.ypRed.cgColor // 3
imageView.layer.cornerRadius = 20 // 4
}
```
@@ -127,6 +128,7 @@ private func showAnswerResult(isCorrect: Bool) {
1. Даём разрешение на рисование рамки;
2. Указываем толщину рамки согласно по макету;
3. С помощью тернарного условного оператора красим рамку в нужный цвет в зависимости от ответа пользователя. Аналогично мы могли бы написать ту же логику через условный оператор `if else`.
4. Задаём радиус скругления согласно макету.
> Тернарный условный оператор вы изучали в уроке «Операторы» темы «Переменные».
@@ -1,46 +1,45 @@
# Введение
В прошлом спринте вы сделали большой рывок! Теперь приложение визуально соответствует финальной версии, получает данные из моков и показывает в виде системного алерта, сколько набрано очков. Дальше вы поработаете с его логикой, качеством кода и взаимодействием с данными — мы поможем со всем разобраться.
> В этом спринте много новой теории и практических задач. Советуем распланировать время так, чтобы точно вникнуть в материал и без спешки выполнить практическое задание. У вас всё получится!
# Цель и задачи на 5 спринт
## Цель и задачи на 5 спринт
**Цель**
### Цель
Снизить общую связность и повысить читабельность кода, реализовать хранение результатов квиза на устройстве. Это нужно, чтобы при повторном открытии приложения вы использовали предыдущие результаты пользователя и вычисляли лучший, как написано в ТЗ.
![image](01.Память/illustration/result_screen_detailed.png)
**Задачи**
### Задачи
1. Найти неточности и усовершенствовать код, с которым вы работали в прошлом спринте.
2. Создать отдельный класс, который будет содержать логику создания нового вопроса квиза.
3. Создать класс для ведения и показа статистики, который вычислит лучший счёт по итогам игровых сессий.
4. Использовать UserDefaults для хранения на устройстве локальных данных о счёте.
# Обзор тем спринта
## Обзор тем спринта
**Тема 1. Память и замыкания**
### Тема 1. Память и замыкания
Вы разберётесь, как работает память устройства и как один символ в коде может привести к утечке данных. Узнаете, что такое замыкание и как передать в одну функцию другую в качестве параметра.
В уроке «Работа с проектом» вы найдете неточности в коде прошлого спринта и усовершенствуете его.
В уроке «Работа с проектом» вы найдёте неточности в коде прошлого спринта и усовершенствуете его.
**Тема 2. Ответственность**
### Тема 2. Ответственность
Вы научитесь разбивать код на более мелкие единицы, а затем соединять в единый механизм, и так исследуете понятие ответственности в коде. Познакомитесь с паттернами проектирования, примените некоторые из них и усовершенствуете качество кода. Ваш код станет удобным для масштабирования, а вероятность случайного краша сведётся к минимуму.
Вы научитесь разбивать код на более мелкие единицы, а затем соединять в единый механизм, и так исследуете понятие ответственности в коде. Познакомитесь с паттернами проектирования, например, такими как «Делегат» и «Инъекция зависимостей», примените некоторые из них и усовершенствуете качество кода. Ваш код станет удобным для масштабирования, а вероятность случайного краша сведётся к минимуму.
Вы создадите файл с фабрикой вопросов — она будет отвечать за создание вопросов квиза и отдавать их список в главный контроллер. Также вы создадите отдельный класс, он вычислит результаты игровой сессии квиза и покажет алерты с ними. В конце темы вы закроете вторую и третью задачу темы.
**Тема 3. Хранение данных**
### Тема 3. Хранение данных
Вы познакомитесь с тем, как гаджет работает с данными, инструментами для их локального хранения, узнаете про форматы передачи данных между устройством и сервером.
Вы потренируетесь использовать UserDefaults и формат передачи данных JSON, чтобы научить приложение хранить данные о счёте в локальном хранилище. Так вы закроете четвёртую задачу спринта.
> Для работы в этом спринте создайте в Git отдельную ветку. Используйте её для работы с проектом `MovieQuiz`. А если вам захочется поэкспериментировать, например изменить название моделей, создайте новую ветку от ветки текущего спринта и поработайте в ней. После вы можете вернуться в ветку для спринта и удалить ветку с экспериментами. Git даёт много возможностей!
> Для работы в этом спринте создайте в Git отдельную ветку. Используйте её для работы с проектом `MovieQuiz`. А если вам захочется поэкспериментировать, например, изменить название моделей, создайте новую ветку от ветки текущего спринта и поработайте в ней. После вы можете вернуться в ветку для спринта и удалить ветку с экспериментами. Git даёт много возможностей!
КНОПКА
Отлично! Перейдём к первой теме
@@ -1,4 +1,4 @@
# Содержание
# Введение в тему
Напомним, в этой теме вы разберётесь с памятью устройства, замыканиями и передачей функций в качестве параметра. Найдёте и исправите ошибки в коде, а если возникнут сложности, посмотрите видеоурок.
@@ -8,7 +8,7 @@
Вы узнаете, как устроена оперативная память, с какими проблемами сталкиваются разработчики при работе с ней и как она научилась очищать себя сама.
## Как приложение хранит данные
## Устройство оперативной памяти: Стек и Куча
Вспомните про различия стека и кучи, а также разберетесь как приложение хранит значимые и ссылочные типы данных.
@@ -1,122 +0,0 @@
## Введение
Когда мобильное приложение работает, в нём создаются и хранятся данные: названия кнопок или лейблов в виде строковых переменных, картинки, счётчики. В приложении могут быть и более сложные типы данных, например модель фильма, вопроса или результата игры.
Мы не можем напрямую контролировать работу операционной системы приложения. Под это выделяется определённая оперативная память устройства. Её можно условно поделить на два участка: Stack (стек) и Heap (кучу) — про них мы уже говорили во втором спринте.
Вы узнаете:
- Как приложение хранит значимые и ссылочные типы данных.
- Что такое стек и куча — и чем они отличаются.
- Что такое принцип LIFO.
КНОПКА Поехали!
# Стек и куча
В оперативной памяти компьютера две области. Их принято называть стеком (от англ. Stack — «стопка») и кучей (от англ. Heap), работают и устроены они по-разному. Разработчики и разработчицы не управляют тем, где будут находиться данные, — за них это делает операционная система.
- Стек используется для хранения данных, к которым нужен быстрый доступ во время выполнения программы. Параметры функций и локальные переменные хранятся в стеке, потому что они часто используются при вызовах функций и работе с локальными переменными внутри функций.
- Куча может хранить большие объёмы данных, но доступ к ней медленный. Она удобна для динамического выделения памяти.
Давайте разбираться с этими двумя областями!
## Стек
Стек характеризуется принципом LIFO (Last In, First Out — «Последним пришёл — первым вышел»). То есть данные, которые мы последними положили в стек, будут первыми взяты оттуда.
Рассмотрим пример простого кода в Playground с функцией, которая добавляет единицу к переданному ей числу:
```swift
var a = 5 // 1
func someFunc(_ a: Int) -> Int { // 2
var result = a + 1 // 3
return result // 4
}
print(someFunc(a)) // 5
```
1. В основной программе мы объявили переменную `a` и присвоили ей значение `5`. Эта переменная помещается в стек.
2. Далее мы объявили функцию `someFunc(_ a: Int) -> Int` — она принимает число и возвращает его.
3. Число, которое было принято в качестве аргумента, увеличивается на единицу.
4. Это число возвращается из функции.
5. В момент вызова функции `someFunc(_ a: Int)` в стек добавляются переменные, которые объявлены внутри неё.
![image](https://user-images.githubusercontent.com/102217910/183639559-7f11c16d-f5a8-45b4-95b5-148c149df635.png)
> Переменная a была скопирована: она — value type, или значимый тип, который передаётся в функцию по значению. После выхода из функции переменные result и a будут сняты со стека. Сверху останется лежать переменная a основной программы.
![image](https://user-images.githubusercontent.com/102217910/183639637-76692205-710a-4c49-aacf-0d3342394aad.png)
Что произойдёт, если в функцию надо будет передать много данных? При создании интернет-магазина в какой-либо функции нам нужно получать всю информацию по определённому товару или бренду, цене, материалам:
```Swift
class Product {
let brand: String
let price: CGFloat
let material: [Material]
...
}
```
Копирование такого объёма данных может быть громоздким для оперативной памяти. Из‑за особенностей организации на физическом уровне у стека есть ограничение по размеру — оно задаётся операционной системой. Тут на помощь приходит куча.
КНОПКА Как она помогает решить проблему?
## Куча
Проблему можно решить так: выделить кучу, в которой хранятся `reference types`, или ссылочные типы — они копируются в функцию не по значению, а по ссылке.
При передаче в функцию, например такую:
```Swift
let a = 5
let product = Product()
func obtain(product: Product) {
...
}
obtain(product: product)
```
Стек и куча будут выглядеть так:
![image](https://user-images.githubusercontent.com/102217910/183639743-eaa69f9a-08ae-4dcb-b4f3-597b37961812.png)
> Стек хранит ссылку на адрес памяти в куче, поэтому мы можем изменять объект внутри функции. Размер кучи условно ограничен оперативной памятью устройства; стек работает быстрее. Где хранятся объекты — определяет операционная система в зависимости от их типов объектов: структур, классов или других типов, ссылочных или значимых.
Вот и вся теория! Давайте, как всегда, закрепим знания в блоке «Проверим изученное». Готовы переходить к вопросу по теме?
КНОПКА Да!
# Проверим изученное
_Прим. автора: это квиз на соотнесение по категориям._
Какие характеристики относятся к стеку, а какие к куче?
Стек
- работает быстрее
- размер ограничен, выделен операционной системой
- хранит значимые типы данных
- работает по принципу LIFO
Куча
- размер ограничен только оперативной памятью
- хранит ссылочные типы данных
# Подведём итоги
Мы рассмотрели, как хранятся данные во время работы программы, и вы узнали, чем отличаются стек и куча. Отлично!
В следующем уроке вы разберётесь, как работает ARC и какие бывают типы ссылок, что такое утечка данных в приложении — и как её не допустить. Переходим дальше?
КНОПКА Да, вперёд!

Some files were not shown because too many files have changed in this diff Show More