Merge pull request #549 from Yandex-Practicum/fix/sprint-5-topic-3-update

правки в спринт 5 тема 3
This commit is contained in:
Harley
2025-10-21 11:17:52 +03:00
committed by GitHub
8 changed files with 250 additions and 69 deletions
@@ -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 @@
# Содержание
# Введение в тему
Привет! Рады видеть вас в новой теме. В ней вы разберётесь, как приложение хранит разные типы данных на устройстве.
@@ -20,7 +20,9 @@
Углубимся в один из основных инструментов для хранения легковесных данных на устройстве — `UserDefaults`. Вы изучите, как с помощью этого инструмента сохранять и читать примитивные типы данных: строки, числа и даты.
## Сохранение данных и статистика MovieQuiz
`UserDefaults` хорошо подходит для хранения небольших объёмов данных, примитивных типов *(числа, строки, логические)*, таких как настройки пользователя или простая статистика. Он прост в использовании и не требует никакой настройки. *Для более объёмных или структурированных данных лучше рассмотреть другие инструменты, например, Core Data (спринт 15).*
## Сохранение данных и статистика MovieQuiz
После теоретического знакомства с `UserDefaults` пора применять знания на практике. Вы разработаете сервис для сохранения и отображения статистики пользователя в приложении MovieQuiz.
@@ -6,7 +6,7 @@
Мы разберём в этой теме:
- что такое `Bundle` и когда он может пригодиться;
- что такое `Bundle` и когда он может пригодиться;
- почему `Sandbox` – особое место в памяти девайса и чем он полезен.
Понимание этих концепций поможет при организации хранения данных приложения, а ещё выделит вас на фоне других кандидатов во время технических интервью. Вопросы про `Sandbox` и `Bundle` часто встречаются в интервью для iOS-разработчиков: знания в этих областях указывают на то, что вы можете обеспечить безопасность приложения и его грамотную архитектуру. Это может стать решающим фактором при найме!
@@ -13,7 +13,7 @@
1. `UserDefaults`.
2. `Keychain`.
3. Файловую систему.
4. Базы данных.
4. Базы данных.
Со всем этим вы познакомитесь на уроке. Начинаем?
@@ -38,9 +38,9 @@ UserDefaults.standard.string(forKey: "currency")
```
> У этого механизма есть недостатки. Вы достаёте значение по строковому ключу — название ключа можно перепутать. В лучшем случае это не даст получить значение, в худшем — приведёт к записи в уже существующее значение другое, которое записывать не нужно.
>
>
> Например, так можно затереть записанное значение по ключу `currency`:
>
>
> `UserDefaults.standard.set(true, forKey: "currency")`
>
> При попытке достать по этому по ключу строку, вы уже не получите ожидаемое `rub`.
@@ -63,13 +63,14 @@ Apple предоставляет отличный интерфейс для вз
Минус этого механизма — крайне низкая скорость чтения из файла и записи в файл.
Мы уже работали с файловой системой через терминал в уроке «Командная строка» темы Git; для работы с файловой системой в iOS есть специальный класс [FileManager](https://developer.apple.com/documentation/foundation/filemanager).
Мы уже работали с файловой системой через терминал в уроке «Командная строка» темы Git; для работы с файловой системой в iOS есть специальный класс [FileManager](https://developer.apple.com/documentation/foundation/filemanager).
Перейдите по ссылке в официальную документацию и найдите методы, которые помогают решать следующие задачи:
- проверить, существует ли какой-то файл или папка,
- проверить, существует ли какой-то файл или папка,
- создать файл или папку,
- удалить файл,
- записать информацию в созданный файл.
- записать информацию в созданный файл.
`FileManager` позволяет выполнять те же операции, что и консольные команды, но только внутри песочницы (ГЛОССАРИЙ: от англ. `Sandbox` — изолированный участок памяти, доступный приложению для хранения временных файлов) приложения на устройстве.
@@ -88,10 +89,11 @@ Apple предоставляет отличный интерфейс для вз
> В приложении разработчики используют разные механизмы для хранения данных локально. Например, Keychain для хранения паролей, а Core Data для моделей данных из сети.
С теорией в уроке закончили. Вам доступно всего четыре варианта хранения данных:
- `UserDefaults`,
- `Keychain`,
- `UserDefaults`,
- `Keychain`,
- базы данных,
- запись в файловую систему.
- запись в файловую систему.
У каждого есть плюсы и минусы, а значит, и задачи, под которые каждый из механизмов подойдёт. Для начинающих разработчиков особенно важно знать эти нюансы: их часто спрашивают на собеседованиях.
@@ -106,7 +108,7 @@ Apple предоставляет отличный интерфейс для вз
// Варианты в левой колонке
1. `UserDefaults`
1. `UserDefaults`
2. `Keychain`
3. Файловая система
4. База данных
@@ -145,7 +147,7 @@ D. Хранение небольших данных настроек и пред
- [x] Небольшие настройки приложения: настройки пользователя, предпочтения, темы оформления и состояния виджетов. (Правильно! Такие данные легко представить с помощью базовых типов `Bool`, `String`, `Int` – они не займут много места, а `UserDefaults` предоставляет удобные методы для хранения и чтения именно базовых типов.)
- [ ] Большие файлы: изображения и видео, которые используются в приложении. (Нет, `UserDefaults` не предназначены для хранения больших файлов: изображений и видео.)
- [x] Метаданные, например информацию о последнем визите пользователя или времени последней синхронизации данных. (Верно! Метаданные — обычно не чувствительная информация; они могут быть представлены базовыми типами, которые удобнее хранить в `UserDefaults`.)
- [x] Метаданные, например, информацию о последнем визите пользователя или времени последней синхронизации данных. (Верно! Метаданные — обычно не чувствительная информация; они могут быть представлены базовыми типами, которые удобнее хранить в `UserDefaults`.)
- [x] Простые значения: строки, числа и булевы значения, которые используются для хранения небольших состояний или настроек. (Да!)
- [ ] Чувствительные данные: пароли или конфиденциальную информацию. (Нет, `UserDefaults` — небезопасное место для хранения чувствительных данных.)
- [x] Индексы, ключи и другие небольшие метаданные, которые улучшают производительность приложения. (Так и есть!)
@@ -157,7 +159,7 @@ D. Хранение небольших данных настроек и пред
- [ ] `UserDefaults` (`UserDefaults` не предназначены для хранения больших объёмов данных.)
- [ ] `Keychain` (`Keychain` используется для хранения конфиденциальных данных, а не для хранения больших данных.)
- [х] Каталог временных файлов внутри песочницы приложения (Верно! Память устройства, выделенная в `Sandbox`, – отличное место для хранения файлов вроде изображений.)
- [ ] База данных (База данных больше подходит для структурированных данных, например для записи истории записей пользователя; для хранения картинок её не используют.)
- [ ] База данных (База данных больше подходит для структурированных данных, например, для записи истории записей пользователя; для хранения картинок её не используют.)
**КОНЕЦ КВИЗА**
**КВИЗ** (Множественный выбор)
@@ -166,8 +168,8 @@ D. Хранение небольших данных настроек и пред
- [x] `fileExists(atPath:)` (Абсолютно точно! Это реальный метод FileManager, используемый для проверки существования файла или папки по указанному пути.)
- [ ] `exists(itemAtPath:)` (Такого метода нет: мы выдумали его, чтобы запутать вас. Правильный метод для проверки существования пути к файлу – `fileExists(atPath:)`.)
- [ ] `deleteForever(path:)` (Такого метода не существует: мы его выдумали. Для удаления файла используется метод `removeItem(at:)`.)
- [x] `urls(for:in:)` (Отлично! C помощью этого метода можно получить URL основных директорий, например к папке Documents.)
- [ ] `set(_:forKey:)` (Такой метод существует, но доступен он у UserDefaults: с его помощью можно сохранить значение примитивного типа, например Bool по ключу.)
- [x] `urls(for:in:)` (Отлично! C помощью этого метода можно получить URL основных директорий, например, к папке Documents.)
- [ ] `set(_:forKey:)` (Такой метод существует, но доступен он у UserDefaults: с его помощью можно сохранить значение примитивного типа, например, Bool по ключу.)
- [ ] `directoryAccess(path:for:)` (Возможно, где-то такой метод и существует, но в документации FileManager его нет!)
- [ ] `string(forKey:)` (Такой метод существует, правда, доступен у UserDefaults: с помощью этого метода можно попытаться достать строковое значение по ключу.)
- [x] `createFile(atPath:contents:attributes:)` (Верно! С помощью этого метода можно записать данные типa Data по указанному пути.)
@@ -197,7 +199,7 @@ print(UserDefaults.standard.string(forKey: "currency"))
- `UserDefaults` как простое хранилище для легковесных пользовательских настроек;
- `Keychain` для безопасного хранения конфиденциальной информации: паролей и ключей шифрования;
- файловую систему для работы с более крупными и сложными структурами данных, например медиафайлами.
- файловую систему для работы с более крупными и сложными структурами данных, например, медиафайлами.
- базы данных для структурированного хранения больших объёмов данных, которые требуют сложных запросов и операций.
В следующем уроке мы разберём `UserDefaults` подробно и сформулируем задачу для улучшения MovieQuiz.
@@ -1,6 +1,6 @@
# UserDefaults
В прошлых уроках вы узнали о способах хранения данных на устройстве, одним из способов был `UserDefaults`. Скоро вы научите приложение MovieQuiz вести статистику игр между сессиями — для этого понадобится сохранять промежуточные данные. Так как хранить нам придётся небольшой объём, `UserDefaults` отлично подойдут для этой задачи.
В прошлых уроках вы узнали о способах хранения данных на устройстве, одним из способов был `UserDefaults`. Скоро вы научите приложение MovieQuiz вести статистику игр между сессиями — для этого понадобится сохранять промежуточные данные. Так как хранить нам придётся небольшой объём, `UserDefaults` отлично подойдут для этой задачи.
> Рекомендуем открыть новый Playground и тестировать каждую строчку, чтобы лучше разобраться с методами `UserDefaults`.
@@ -31,7 +31,7 @@ UserDefaults.standard.dictionary(forKey: "stores")
![image](./images/user_defaults_examples.png)
> Вы всегда можете посмотреть [в документации Apple](https://developer.apple.com/documentation/foundation/userdefaults/1408563-dictionary), с какими возвращаемыми типами данных работают `UserDefaults`. Если коротко, то поддерживаются все примитивы, которые используются в Swift.
>
>
> Имейте в виду: почти всегда при этом вы будете получать опционал, ведь значения по переданному ключу может не оказаться. Если чисел и булевых значений в хранилище нет — Swift вернёт `0` и `false`. Это поведение может меняться в зависимости от версии языка.
**СКРЫВАШКА**
@@ -47,7 +47,7 @@ UserDefaults.standard.dictionary(forKey: "stores")
- NSArray,
- NSDictionary.
До macOS и iOS существовала операционная система `NeXTSTEP`; мы говорили о ней в теме про реализацию логики по макету. Наработки по описанию типов в `NeXTSTEP` легли в основу фреймворка Foundation — мы им пользуемся для написания приложений под системы Apple.
До macOS и iOS существовала операционная система `NeXTSTEP`; мы говорили о ней в теме про реализацию логики по макету. Наработки по описанию типов в `NeXTSTEP` легли в основу фреймвока Foundation — мы им пользуемся для написания приложений под системы Apple.
В `NeXTSTEP` писали код на Objective-C. В название классов добавляли префикс NS, чтобы другие разработчики визуально отличали код `NeXTSTEP` от другого кода.
@@ -63,7 +63,7 @@ UserDefaults.standard.dictionary(forKey: "stores")
UserDefaults.standard.set("rub", forKey: "currency")
```
В качестве первого параметра принимается значение типа `Any?`, в качестве второго аргумента — ключ, по которому значение будет храниться.
В качестве первого параметра принимается значение типа `Any?`, в качестве второго аргумента — ключ, по которому значение будет храниться.
**СКРЫВАШКА**
Что такое тип `Any`
@@ -86,11 +86,11 @@ let arrayOfAny: [Any] = [1, "2", { return 3}]
Для проекта MovieQuiz нам понадобится сохранить дату лучшей игры. `UserDefaults` поддерживает сохранение примитива `Date`, поэтому мы можем легко это сделать, например, так:
```swift
```swift
UserDefaults.standard.set(Date(), forKey: "bestGame.date")
```
Однако красивого метода вроде `date(forKey:)` нам не предоставили, и единственный способ получить дату — это воспользоваться методом `object(forKey:)`, который возвращает опционал `Any?`. В таком случае нам нужно привести тип к `Date` с помощью оператора `as?`:
Однако красивого метода вроде `date(forKey:)` нам не предоставили, и единственный способ получить дату — это воспользоваться методом `object(forKey:)`, который возвращает опционал `Any?`. В таком случае нам нужно привести тип к `Date` с помощью оператора `as?`:
```swift
if let date = UserDefaults.standard.object(forKey: "bestGame.date") as? Date {
@@ -101,7 +101,7 @@ else {
}
```
Этот код можно записать короче с помощью оператора `Nil-Coalescing (`??`)`. Попробуйте упростить эту запись самостоятельно.
Этот код можно записать короче с помощью оператора `Nil-Coalescing (`??`)`. Попробуйте упростить эту запись самостоятельно.
**СКРЫВАШКА**
**Авторское решение**
@@ -113,13 +113,14 @@ else {
```swift
let date = UserDefaults.standard.object(forKey: "bestGame.date") as? Date ?? Date()
```
**СКРЫВАШКА-КОНЕЦ**
### Работа с UserDefaults как хранилищем всех данных
В этом спринте вам нужно будет сохранить много промежуточных данных, чтобы написать алгоритм ведения статистики игр. В процессе отладки алгоритма можно записать лишнего или записать данные по ошибочному ключу. В таком случае хочется иметь способ «заглянуть внутрь» и узнать, что же записалось в хранилище, — или наоборот, сбросить всё к «заводским»: очистить хранилище.
В этом спринте вам нужно будет сохранить много промежуточных данных, чтобы написать алгоритм ведения статистики игр. В процессе отладки алгоритма можно записать лишнего или записать данные по ошибочному ключу. В таком случае хочется иметь способ «заглянуть внутрь» и узнать, что же записалось в хранилище, — или наоборот, сбросить всё к «заводским»: очистить хранилище.
Для первой задачи понадобится метод `dictionaryRepresentation`:
Для первой задачи понадобится метод `dictionaryRepresentation`:
```swift
func dictionaryRepresentation() -> [String: Any]
@@ -137,7 +138,7 @@ for (key, value) in allValues {
}
```
Если вам нужно удалить все сохранённые данные из `UserDefaults` (например, при выходе пользователя из приложения), сперва получите все объекты, затем вызовите на них `removeObject(forKey:)`:
Если вам нужно удалить все сохранённые данные из `UserDefaults` (например, при выходе пользователя из приложения), сперва получите все объекты, затем вызовите на них `removeObject(forKey:)`:
```swift
// Получаем словарь всех значений
@@ -158,7 +159,7 @@ allValues.keys.forEach { key in
- [ ] `Double`
Тип `Double` можно напрямую записать и прочитать из `UserDefaults`.
- [x] `UIImage`
- [x] `UIImage`
Верно! UIImage не может быть сохранён напрямую в `UserDefaults`. Вместо этого его нужно сначала преобразовать в Data (используя `UIImageJPEGRepresentation` или `UIImagePNGRepresentation`), а затем сохранить как Data.
- [ ] `Bool`
@@ -214,12 +215,77 @@ allValues.keys.forEach { key in
1. Откройте приложение Counter, которое вы разработали ранее. Добавьте приложению функцию сохранения состояния: научите сохранять значение счётчика и получать его на старте приложения.
Во второй и последующие запуски приложения вы должны видеть последнее посчитанное значение. Если вы разработали усложнённую версию приложения, научите его сохранять историю изменинй.
Во второй и последующие запуски приложения вы должны видеть последнее посчитанное значение. Если вы разработали усложнённую версию приложения, научите его сохранять историю изменений.
**СКРЫВАШКА**
### Авторское решение
```swift
// Пример сохранения значения счетчика
UserDefaults.standard.set(x, forKey: "counterValue") // Где x - это текущее значение счетчика
// Пример чтения значения счетчика
let loadedCount = UserDefaults.standard.integer(forKey: "counterValue")
```
```swift
// Пример сохранения истории изменений (массив Int)
UserDefaults.standard.set(array, forKey: "counterHistory") // Где array - это массив значений счетчика
// Пример загрузки истории изменений
if let loadedHistory = UserDefaults.standard.array(forKey: "counterHistory") as? [Int] {
// print("Загруженная история: \(loadedHistory)")
}
```
**КОНЕЦ СКРЫВАШКИ**
2. Разработайте приложение с экраном входа в него. На экран добавьте поля для ввода имени пользователя и пароля. Добавьте переключатель (UISwitch), позволяющий сохранять введённое имя пользователя в `UserDefaults`.
При следующем запуске приложения имя пользователя должно автоматически подставляться в соответствующее поле. Если переключатель остаётся выключенным — при повторном входе поле должно оказаться пустым. Макет приложения может быть любым.
**СКРЫВАШКА**
### Авторское решение
**Сохранение:**
```swift
// Пример сохранения имени пользователя, если переключатель включен
let usernameToSave = "user123" // Имя пользователя из текстового поля
let shouldSaveUsername = true // Состояние UISwitch
if shouldSaveUsername {
UserDefaults.standard.set(usernameToSave, forKey: "savedUsername")
} else {
UserDefaults.standard.removeObject(forKey: "savedUsername")
}
// Сохраняем состояние самого переключателя, чтобы восстановить его при следующем запуске
UserDefaults.standard.set(shouldSaveUsername, forKey: "saveUsernamePreference")
```
**Восстановление:**
```swift
// При запуске восстанавливаем состояние переключателя
let shouldLoadSavedUsername = UserDefaults.standard.bool(forKey: "saveUsernamePreference")
// Здесь код для установки состояния UISwitch из shouldLoadSavedUsername
if shouldLoadSavedUsername {
if let savedUsername = UserDefaults.standard.string(forKey: "savedUsername") {
// Здесь код для подстановки savedUsername в текстовое поле
} else {
// print("Сохраненное имя пользователя не найдено.")
}
}
```
> Обратите внимание: сохранять пароли в `UserDefaults` небезопасно. Для этого существуют более защищённые механизмы, такие как Keychain. В данном примере мы фокусируемся только на сохранении имени пользователя.
**КОНЕЦ СКРЫВАШКИ**
![image](./images/practical_user_defaults.png)
КНОПКА
@@ -228,6 +294,7 @@ allValues.keys.forEach { key in
## Подведём итоги
Вы изучили:
- какие типы данных можно хранить в `UserDefaults`;
- как записывать и читать данные из этого хранилища;
- как обрабатывать ситуации, когда методы чтения значения возвращают опциональный тип `Any`;
@@ -2,11 +2,18 @@
Привет! Вы уже знаете про разделение ответственности между классами и хранением данных в устройстве. Будем учиться новому: в уроке вы добавите работу с `UserDefaults` в MovieQuiz и подготовите сервис для подсчёта статистики по итогам игровой сессии. Сервис будет отвечать за хранение и чтение данных из локальной памяти устройства с помощью `UserDefaults`.
В этом уроке мы сначала подробно разберём создание ключевых компонентов сервиса статистики: вы с пошаговыми инструкциями и подсказками научитесь реализовывать сохранение и получение данных о количестве сыгранных игр, лучшем результате и средней точности. Это поможет вам отработать основные механики работы с `UserDefaults`. В завершение вам предстоит уже без детальных инструкций собрать все части воедино, реализовать недостающую логику и полностью интегрировать сервис статистики в приложение MovieQuiz.
А какие данные нужно хранить локально в приложении? Рассмотрим его макет, чтобы узнать ответ.
![image](images/gameFinishAlert.png)
Чтобы вывести пользователю статистику игр, нужно хранить результаты локально между сессиями, иначе подсчитать общую статистику не получится. Пользователь в конце игры видит текущий результат, количество игр, лучший результат с датой и среднюю точность в процентах.
Чтобы вывести пользователю статистику игр, нужно хранить результаты локально между сессиями, иначе подсчитать общую статистику не получится. Пользователь в конце игры видит текущий результат:
- **Результат последнего раунда**: Это количество правильных ответов и общее число вопросов в только что завершённом квизе. Эти данные поступают непосредственно из текущей игровой сессии и не требуют долгосрочного хранения.
- **Общее количество сыгранных квизов**: Это счётчик всех завершённых игр. Он должен хранится; увеличивается на единицу после каждого сыгранного квиза.
- **Рекорд с датой раунда**: Это лучший результат (максимальное количество правильных ответов за одну игру) за всё время и дата, когда этот рекорд был установлен. Эти данные должны сохраняться. После каждой игры текущий результат сравнивается с сохранённым рекордом, и если он лучше, рекорд обновляется.
- **Средняя точность в процентах**: Это отношение общего количества правильных ответов во всех сыгранных квизах к общему количеству вопросов во всех сыгранных квизах. Для расчёта этого значения рекомендуется хранить два отдельных накопительных счётчика: общее количество всех правильных ответов (`totalCorrectAnswers`) и общее количество всех заданных вопросов (`totalQuestionsAsked`). Эти счётчики обновляются после каждого квиза.
Вы добавите в `UserDefaults` данные по результатам каждой игры, а затем будете забирать данные для отображения статистики на алерте в конце раунда.
@@ -52,7 +59,6 @@ protocol StatisticServiceProtocol {
Чтобы показать пользователю лучший результат, в конце каждого раунда мы будем:
- запоминать количество сыгранных игр;
- сравнивать результат текущей игры с рекордом, который хранится в `UserDefaults`;
- обновлять рекорд, если новый результат лучше — то есть если пользователь ответил правильно на большее количество вопросов.
@@ -121,26 +127,26 @@ protocol StatisticServiceProtocol {
}
```
> Осталось только написать реализацию протокола и реализовать полный текст для алерта. Если вы уверены в знаниях по работе с `UserDefaults`, от этой точки можете реализовать протокол сами, а потом сравнить решение с подходом, который мы предлагаем в уроке.
> Осталось только написать реализацию протокола и реализовать полный текст для алерта. Если вы уверены в знаниях по работе с `UserDefaults`, от этой точки можете реализовать протокол сами, а потом сравнить решение с подходом, который мы предлагаем в уроке.
![image](./images/protocol_example.png)
## Реализуем `StatisticServiceProtocol`
После того как мы добавим все нужные свойства и методы в протокол, можно приступать к реализации интерфейса. Создайте новый файл с классом `StatisticService`.
После того как мы добавим все нужные свойства и методы в протокол, можно приступать к реализации интерфейса. Создайте новый файл с классом `StatisticService`.
Расширить реализацию протоколом можно двумя способами:
1. **Прямой реализацией в типе**. Классы, структуры или перечисления могут напрямую поддерживать протокол, если реализуют требования в своём определении: с помощью двоеточий (`:`) и записи названия протокола или протоколов через запятую. Это самый прямой и понятный способ.
```swift
```swift
// Расширяем при объявлении
final class StatisticService: StatisticServiceProtocol {
...
}
```
2. **Использованием расширения** — с помощью ключевого слова `extension`, имени типа, двоеточия и названия протокола. Этот способ удобен для организации кода, например когда внутренняя и приватная реализация остаются описанными внутри блока с классом, а публичная часть для протокола будет описана внутри расширения.
2. **Использованием расширения** — с помощью ключевого слова `extension`, имени типа, двоеточия и названия протокола. Этот способ удобен для организации кода, например, когда внутренняя и приватная реализация остаются описанными внутри блока с классом, а публичная часть для протокола будет описана внутри расширения.
```swift
final class StatisticService {
@@ -169,11 +175,11 @@ extension StatisticService: StatisticServiceProtocol {
### Счётчик игр: `gamesCount`
`var gamesCount: Int` – счётчик сыгранных игр. Он нужен, чтобы сформировать сообщение в алерте; пригодится для подсчёта средней точности ответов. Для работы с полем нам нужно уметь читать значение из `UserDefaults`, чтобы показывать сообщение и статистика велась точно.
`var gamesCount: Int` – счётчик сыгранных игр. Он нужен, чтобы сформировать сообщение в алерте, а так же пригодится для подсчёта средней точности ответов. Для работы с полем нам нужно уметь читать значение из `UserDefaults`, чтобы показывать сообщение, и статистика велась точно.
Давайте потренируемся! Вам понадобится метод для чтения целых чисел `integer(forKey:)`, а для записи подойдёт метод `set(_, forKey:)`.
#### Практическое задание №1
#### Практическое задание №1
Откройте проект и реализуйте это свойство с помощью геттера и сеттера:
@@ -218,7 +224,7 @@ gamesCount += 1 // прочитаем текущее значение и зап
### Лучшая игра: `bestGame`
Чтобы получить и записать рекорд, также воспользуйтесь геттером и сеттером. Структура описана тремя свойствами: количеством вопросов, ответов и датой завершения квиза.
Чтобы получить и записать рекорд, также воспользуйтесь геттером и сеттером. Структура описана тремя свойствами: количеством вопросов, ответов и датой завершения квиза.
#### Практическое задание №2
@@ -248,25 +254,33 @@ var bestGame: GameResult {
### Средняя точность: `totalAccuracy`
Средняя точность ответов — это процент правильных ответов от общего числа вопросов. Вы можете **рассчитать её**, используя сохранённые данные о количестве завершённых игр и количестве правильных ответов за всё время. Для этого подготовьте приватную реализацию хранения и чтения промежуточных данных: заведите приватное свойство `correctAnswers: Int` так же, как вы сохраняли количество игр.
Средняя точность ответов — это процент правильных ответов от общего числа вопросов. Вы можете **рассчитать её**, используя сохранённые данные о количестве заданных вопросов и количестве правильных ответов за всё время. Для этого подготовьте приватную реализацию хранения и чтения промежуточных данных: заведите приватные свойства `correctAnswers: Int` и `totalQuestionsAsked: Int`, так же, как вы сохраняли количество сыгранных игр.
#### Практическое задание №3
Реализуйте вычисление средней точности с помощью промежуточно сохранённых данных об ответах и количестве игр:
Реализуйте вычисление средней точности. Это отношение общего количества правильных ответов за все игры к общему количеству вопросов, заданных за все игры, выраженное в процентах. *(В MovieQuiz всегда задаётся 10 вопросов, поэтому если принять, что это значение не будет меняться никогда, то общее количество вопросов можно рассчитать умножив количество сыгранных игр на 10 — в этом случае, дополнительно хранить ничего не потребуется)*.
```swift
Для этого вам понадобится:
1. Обеспечить сохранение и чтение общего количества правильных ответов за все игры (например, через приватное свойство `totalCorrectAnswers: Int`).
2. Обеспечить сохранение и чтение общего количества вопросов, заданных за все игры (например, через приватное свойство `totalQuestionsAsked: Int`).
3. Использовать эти два значения для вычисления `totalAccuracy`.
```swift
var totalAccuracy: Double {
// отношение всех правильных ответов от общего числа вопросов
// отношение общего числа правильных ответов
// ко всем заданным вопросам за все игры
}
```
**СКРЫВАШКА** **Подсказка**
- Значение `totalAccuracy` можно привести к процентам, если умножить отношение на 100.
- При вычислении `totalAccuracy` может возникнуть деление на 0 и приложение может словить крэш. Предварительно стоит проверить, что общее число вопросов не равно нулю.
- Значение `totalAccuracy` приводится к процентам умножением отношения на 100.
- При вычислении `totalAccuracy` важно избегать деления на ноль (в противном случае может произойти крэш). Если `totalQuestionsAsked` равно нулю, то среднюю точность можно считать равной 0.
**ФОРМУЛА В LATEX**
\[
\text{Общая средняя точность} = \frac{\text{Общее количество правильных ответов за все игры}}{10 \times \text{Количество игр}} \times 100\%
\text{Общая средняя точность} = \frac{\text{Общее количество правильных ответов за все игры}}{\text{общее количество вопросов за все игры}} \times 100\%
\]
**ФОРМУЛА-КОНЕЦ**
@@ -281,7 +295,7 @@ var totalAccuracy: Double {
Во-первых, почти в каждом свойстве приходится писать длинное обращение `UserDefaults.standard`: читать код становится тяжелее с каждой новой строчкой, а код класса становится связным — легко подменить реализацию не получится.
Во-вторых, каждый раз, когда нам нужно обратиться к какому-нибудь значению, мы пишем ключ значения строчкой. Это не самый безопасный способ, потому что можно ошибиться в названии ключа: пропустить букву или скопировать не тот ключ.
Во-вторых, каждый раз, когда нам нужно обратиться к какому-нибудь значению, мы пишем ключ значения строчкой. Это не самый безопасный способ, потому что можно ошибиться в названии ключа: пропустить букву или скопировать не тот ключ.
### Убираем дублирование в коде: обращения к `UserDefaults`
@@ -303,12 +317,12 @@ final class StatisticService {
}
```
Откройте проект и внесите изменения: создайте свойство `storage` и обновите все места обращения.
Откройте проект и внесите изменения: создайте свойство `storage` и обновите все места обращения.
**Кнопка-реплика**
Так код стал чище, ура!
### Убираем дублирование в коде: строковые ключи
### Убираем дублирование в коде: строковые ключи
Теперь давайте решим потенциальные ошибки с ключами.
@@ -318,9 +332,12 @@ final class StatisticService {
```Swift
private enum Keys: String {
case correct
case bestGame
case gamesCount
case gamesCount // Для счётчика сыгранных игр
case bestGameCorrect // Для количества правильных ответов в лучшей игре
case bestGameTotal // Для общего количества вопросов в лучшей игре
case bestGameDate // Для даты лучшей игры
case totalCorrectAnswers // Для общего количества правильных ответов за все игры
case totalQuestionsAsked // Для общего количества вопросов, заданных за все игры
}
```
@@ -340,25 +357,25 @@ var gamesCount: Int {
**КНОПКА**
Вижу. Окей!
## Cохраняем результаты игры
## Сохраняем результаты игры
Сейчас вы подготовили всё необходимое, чтобы получать данные об играх. Осталось реализовать сохранение данных, а именно метод `func store(correct count: Int, total amount: Int)`.
Этот метод принимает результат игры: количество правильных ответов и общее число заданных вопросов по окончании, а также дату, когда игра была закончена.
Этот метод принимает результат игры: количество правильных ответов и общее число заданных вопросов по окончании.
Вам нужно:
– обновить количество заданных вопросов и количество правильных ответов,
– обновить количество сыгранных игр,
– обновить количество сыгранных игр,
– проверить, не получен ли лучший результат игры, чтобы записать рекорд.
После сохранения данных вы сможете показать статистику игр в алерте.
После сохранения данных вы сможете показать статистику игр в алерте.
Пора завершить задачу с вычислением статистики и хранением данных по играм. Всё обязательно получится!
## Самостоятельная работа
Добавьте код, чтобы реализовать функциональность хранения данных и подсчёта статистики.
Добавьте код, чтобы реализовать функциональность хранения данных и подсчёта статистики.
Вам нужно:
@@ -398,7 +415,7 @@ private func show(quiz result: QuizResultsViewModel)
Вы проделали огромную работу: узнали, как хранят информацию в iOS, научились работать с хранилищем `UserDefaults` и научили приложение MovieQuiz вести статистику между сессиями.
Эти знания пригодятся вам не только в проектах курса, но и при прохождении собеседований.
Эти знания пригодятся вам не только в проектах курса, но и при прохождении собеседований.
Пятый спринт был объёмным, и мы подготовили небольшую награду — [уникальный стикерпак](https://t.me/addstickers/practicum_ios) для вас. Вот несколько стикеров из набора:
@@ -0,0 +1,57 @@
# Задание по теме
> Напоминаем, что задания нужно делать в отдельной ветке в Github с названием **sprint_05**.
В качестве домашнего задания предлагаем вам добавить оставшийся нужный код, чтобы реализовать функциональность хранения данных и подсчёта статистики.
## Вам понадобится:
- Реализовать геттеры и сеттеры для оставшихся свойств, собрав их в класс `StatisticService`,
- Реализовать функцию сохранения лучшего результата `store` (с проверкой на то, что новый результат лучше сохранённого в `User Defaults`),
- К самому алерту в поле `message` передать сформированный из рекордной игры текст.
Не забудьте, что вам нужно создать экземпляр класса `StatisticService`, чтобы работать с ним в `MovieQuizViewController`!
## Убедитесь, что:
1. Приложение запускается.
2. Приложение выглядит корректно — элементы интерфейса растягиваются и адаптируются под разные разрешения экранов iPhone (начиная с X вёрстка под SE и iPad не предусмотрена).
3. По окончании раунда вызывается алерт, который соответствует макету и на котором отображаются следующие данные:
- Текущий результат в виде соотношения правильно отвеченных в раунде вопросов и общего количества вопросов в одном раунде.
- Количество сыгранных квизов за всё время, что установлено приложение.
- Лучший результат за всё время, что приложение установлено. В той же строке должна выводится дата достижения этого результата в формате, соответствующем макетам.
- Средняя точность, рассчитывающаяся как отношение правильно отвеченных вопросов за всё время игры к общему количеству вопросов за всё время игры. Точность должна быть выражена в процентах.
4. Ваш класс `StatisticService` закрыт протоколом `StatisticServiceProtocol`, и в `MovieQuizViewController` используется переменная `statisticService` типа `StatisticServiceProtocol`.
5. Попробуйте завершить приложение, выкинув его из памяти и запустив снова. Ваши результаты, которые показываются в конце раунда, не должны обнулиться.
> Не забывайте обращаться к наставникам, если возникнут вопросы!
## Подсказки
- `message` для алерта нужно будет заменить в функции
```swift
private func show(quiz result: QuizResultsViewModel)
```
- для округления точности можно воспользоваться конструкцией
```swift
"\(String(format: "%.2f", statisticService.totalAccuracy))%"
// где если statisticService.totalAccuracy = 95.5, то строка будет "95.50%"
```
- сервис по статистике можно инициализировать во viewDidLoad (помните, что сама переменная должна быть типа протокола)
```swift
statisticService = StatisticService()
```
- текущую дату можно получить следующим образом: `Date()`
- дату в нужном форматировании можно получить через `dateTimeString`, что лежит в файле `Date+Extensions.swift` в папке `Helpers`
для сравнения двух рекордов нужно сравнить их поля `correct` (создав, например, метод для этого в структуре `GameRecord`)
перенос строки можно сделать, добавив в строку `\n`
---
Мы проделали очень большую работу! Познакомились с вариантами хранения информации в iOS и не только, научились работать с разными хранилищами, попробовали сохранять данные в файл, в `User Defaults`.
@@ -0,0 +1,37 @@
# Итоги спринта 05
В этом спринте вы углубились в важные аспекты iOS-разработки, которые помогут вам писать более качественный, поддерживаемый и эффективный код. Мы рассмотрели управление памятью, принципы проектирования и способы хранения данных на устройстве.
## Ключевые темы и достижения
### 1. Память
В этом блоке мы подробно разобрались с **оперативной памятью** и проблемами управления ею, такими как утечки памяти и "мусор". Важным моментом стало изучение механизма **ARC (Automatic Reference Counting)** в Swift.
Мы также изучили **ссылки**, поняв разницу между сильными (`strong`) и слабыми (`weak`) ссылками. Вы научились использовать `weak` ссылки для разрыва **циклов сильных ссылок (Retain Cycles)**.
**Замыкания (Closures)** стали ещё одной важной темой. Вы узнали, что это анонимные функции, способные захватывать и хранить ссылки на переменные и константы из окружающего их контекста. Мы рассмотрели, как **захват переменных** замыканиями. Наконец, мы коснулись **функций высшего порядка**, которые могут принимать другие функции в качестве параметров или возвращать их как результат.
### 2. Ответственность и паттерны проектирования
Центральной идеей этого раздела стало **понятие ответственности в коде**. Проведя аудит `MovieQuizViewController`, мы выявили множественные ответственности и поняли, что их разделение улучшает читаемость, тестируемость и масштабируемость кода.
На практике мы применили эти принципы, создав **фабрику вопросов (`QuestionFactory`)** как отдельный сервис. Этот сервис теперь отвечает за создание и предоставление вопросов для квиза.
Важной частью было налаживание **взаимодействия между классами**, в частности, между `MovieQuizViewController` и `QuestionFactory`.
Для организации этого взаимодействия мы изучили два ключевых **паттерна проектирования**. Первым был паттерн **"Делегат" (Delegation Pattern)**, а также поняли, как использовать протоколы для определения контракта делегирования и `weak` ссылки для избежания циклов.
Вторым паттерном стала **"Инъекция зависимостей" (Dependency Injection - DI)**. Вы узнали, что DI - это способ предоставления объекту его зависимостей. Мы рассмотрели три способа инъекции (через инициализатор, свойство, метод).
### 3. Хранение данных
В заключительном блоке мы рассмотрели, как приложения хранят данные. Мы обсудили различные **способы хранения данных на устройстве**, такие как `Keychain` и сохранение в файлы, но подробно остановились на **UserDefaults**. Вы детально изучили `UserDefaults` как простой инструмент для хранения небольших объёмов данных.
## Результат
По итогам этого спринта ваше приложение `MovieQuiz` стало значительно более структурированным, надёжным и подготовленным к дальнейшему развитию.
Теперь вы готовы к следующему спринту, где мы будем работать с сетью. В следующем этапе вы изучите, как загружать данные из интернета, интегрировать сетевые сервисы и обрабатывать полученную информацию. Все изученные концепции, такие как разделение ответственности, использование паттернов "Делегат" и "Инъекция зависимостей", а также навыки работы с хранением данных, станут основой для создания более сложного и функционального приложения.
Поздравляем с успешным завершением спринта! Впереди вас ждут новые интересные задачи и углубление в мир iOS-разработки.