Merge pull request #549 from Yandex-Practicum/fix/sprint-5-topic-3-update
правки в спринт 5 тема 3
This commit is contained in:
@@ -1,46 +1,45 @@
|
||||
|
||||
# Введение
|
||||
|
||||
В прошлом спринте вы сделали большой рывок! Теперь приложение визуально соответствует финальной версии, получает данные из моков и показывает в виде системного алерта, сколько набрано очков. Дальше вы поработаете с его логикой, качеством кода и взаимодействием с данными — мы поможем со всем разобраться.
|
||||
|
||||
> В этом спринте много новой теории и практических задач. Советуем распланировать время так, чтобы точно вникнуть в материал и без спешки выполнить практическое задание. У вас всё получится!
|
||||
|
||||
# Цель и задачи на 5 спринт
|
||||
## Цель и задачи на 5 спринт
|
||||
|
||||
**Цель**
|
||||
### Цель
|
||||
|
||||
Снизить общую связность и повысить читабельность кода, реализовать хранение результатов квиза на устройстве. Это нужно, чтобы при повторном открытии приложения вы использовали предыдущие результаты пользователя и вычисляли лучший, как написано в ТЗ.
|
||||
|
||||

|
||||
|
||||
**Задачи**
|
||||
### Задачи
|
||||
|
||||
1. Найти неточности и усовершенствовать код, с которым вы работали в прошлом спринте.
|
||||
2. Создать отдельный класс, который будет содержать логику создания нового вопроса квиза.
|
||||
3. Создать класс для ведения и показа статистики, который вычислит лучший счёт по итогам игровых сессий.
|
||||
4. Использовать UserDefaults для хранения на устройстве локальных данных о счёте.
|
||||
|
||||
# Обзор тем спринта
|
||||
## Обзор тем спринта
|
||||
|
||||
**Тема 1. Память и замыкания**
|
||||
### Тема 1. Память и замыкания
|
||||
|
||||
Вы разберётесь, как работает память устройства и как один символ в коде может привести к утечке данных. Узнаете, что такое замыкание и как передать в одну функцию другую в качестве параметра.
|
||||
|
||||
В уроке «Работа с проектом» вы найдете неточности в коде прошлого спринта и усовершенствуете его.
|
||||
В уроке «Работа с проектом» вы найдёте неточности в коде прошлого спринта и усовершенствуете его.
|
||||
|
||||
**Тема 2. Ответственность**
|
||||
### Тема 2. Ответственность
|
||||
|
||||
Вы научитесь разбивать код на более мелкие единицы, а затем соединять в единый механизм, и так исследуете понятие ответственности в коде. Познакомитесь с паттернами проектирования, примените некоторые из них и усовершенствуете качество кода. Ваш код станет удобным для масштабирования, а вероятность случайного краша сведётся к минимуму.
|
||||
Вы научитесь разбивать код на более мелкие единицы, а затем соединять в единый механизм, и так исследуете понятие ответственности в коде. Познакомитесь с паттернами проектирования, например, такими как «Делегат» и «Инъекция зависимостей», примените некоторые из них и усовершенствуете качество кода. Ваш код станет удобным для масштабирования, а вероятность случайного краша сведётся к минимуму.
|
||||
|
||||
Вы создадите файл с фабрикой вопросов — она будет отвечать за создание вопросов квиза и отдавать их список в главный контроллер. Также вы создадите отдельный класс, он вычислит результаты игровой сессии квиза и покажет алерты с ними. В конце темы вы закроете вторую и третью задачу темы.
|
||||
|
||||
**Тема 3. Хранение данных**
|
||||
### Тема 3. Хранение данных
|
||||
|
||||
Вы познакомитесь с тем, как гаджет работает с данными, инструментами для их локального хранения, узнаете про форматы передачи данных между устройством и сервером.
|
||||
|
||||
Вы потренируетесь использовать UserDefaults и формат передачи данных JSON, чтобы научить приложение хранить данные о счёте в локальном хранилище. Так вы закроете четвёртую задачу спринта.
|
||||
|
||||
> Для работы в этом спринте создайте в Git отдельную ветку. Используйте её для работы с проектом `MovieQuiz`. А если вам захочется поэкспериментировать, например изменить название моделей, создайте новую ветку от ветки текущего спринта и поработайте в ней. После вы можете вернуться в ветку для спринта и удалить ветку с экспериментами. Git даёт много возможностей!
|
||||
> Для работы в этом спринте создайте в Git отдельную ветку. Используйте её для работы с проектом `MovieQuiz`. А если вам захочется поэкспериментировать, например, изменить название моделей, создайте новую ветку от ветки текущего спринта и поработайте в ней. После вы можете вернуться в ветку для спринта и удалить ветку с экспериментами. Git даёт много возможностей!
|
||||
|
||||
КНОПКА
|
||||
Отлично! Перейдём к первой теме
|
||||
|
||||
+4
-2
@@ -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.
|
||||
|
||||
+79
-12
@@ -1,6 +1,6 @@
|
||||
# UserDefaults
|
||||
|
||||
В прошлых уроках вы узнали о способах хранения данных на устройстве, одним из способов был `UserDefaults`. Скоро вы научите приложение MovieQuiz вести статистику игр между сессиями — для этого понадобится сохранять промежуточные данные. Так как хранить нам придётся небольшой объём, `UserDefaults` отлично подойдут для этой задачи.
|
||||
В прошлых уроках вы узнали о способах хранения данных на устройстве, одним из способов был `UserDefaults`. Скоро вы научите приложение MovieQuiz вести статистику игр между сессиями — для этого понадобится сохранять промежуточные данные. Так как хранить нам придётся небольшой объём, `UserDefaults` отлично подойдут для этой задачи.
|
||||
|
||||
> Рекомендуем открыть новый Playground и тестировать каждую строчку, чтобы лучше разобраться с методами `UserDefaults`.
|
||||
|
||||
@@ -31,7 +31,7 @@ UserDefaults.standard.dictionary(forKey: "stores")
|
||||

|
||||
|
||||
> Вы всегда можете посмотреть [в документации 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. В данном примере мы фокусируемся только на сохранении имени пользователя.
|
||||
|
||||
**КОНЕЦ СКРЫВАШКИ**
|
||||
|
||||

|
||||
|
||||
КНОПКА
|
||||
@@ -228,6 +294,7 @@ allValues.keys.forEach { key in
|
||||
## Подведём итоги
|
||||
|
||||
Вы изучили:
|
||||
|
||||
- какие типы данных можно хранить в `UserDefaults`;
|
||||
- как записывать и читать данные из этого хранилища;
|
||||
- как обрабатывать ситуации, когда методы чтения значения возвращают опциональный тип `Any`;
|
||||
+45
-28
@@ -2,11 +2,18 @@
|
||||
|
||||
Привет! Вы уже знаете про разделение ответственности между классами и хранением данных в устройстве. Будем учиться новому: в уроке вы добавите работу с `UserDefaults` в MovieQuiz и подготовите сервис для подсчёта статистики по итогам игровой сессии. Сервис будет отвечать за хранение и чтение данных из локальной памяти устройства с помощью `UserDefaults`.
|
||||
|
||||
В этом уроке мы сначала подробно разберём создание ключевых компонентов сервиса статистики: вы с пошаговыми инструкциями и подсказками научитесь реализовывать сохранение и получение данных о количестве сыгранных игр, лучшем результате и средней точности. Это поможет вам отработать основные механики работы с `UserDefaults`. В завершение вам предстоит уже без детальных инструкций собрать все части воедино, реализовать недостающую логику и полностью интегрировать сервис статистики в приложение MovieQuiz.
|
||||
|
||||
А какие данные нужно хранить локально в приложении? Рассмотрим его макет, чтобы узнать ответ.
|
||||
|
||||

|
||||
|
||||
Чтобы вывести пользователю статистику игр, нужно хранить результаты локально между сессиями, иначе подсчитать общую статистику не получится. Пользователь в конце игры видит текущий результат, количество игр, лучший результат с датой и среднюю точность в процентах.
|
||||
Чтобы вывести пользователю статистику игр, нужно хранить результаты локально между сессиями, иначе подсчитать общую статистику не получится. Пользователь в конце игры видит текущий результат:
|
||||
|
||||
- **Результат последнего раунда**: Это количество правильных ответов и общее число вопросов в только что завершённом квизе. Эти данные поступают непосредственно из текущей игровой сессии и не требуют долгосрочного хранения.
|
||||
- **Общее количество сыгранных квизов**: Это счётчик всех завершённых игр. Он должен хранится; увеличивается на единицу после каждого сыгранного квиза.
|
||||
- **Рекорд с датой раунда**: Это лучший результат (максимальное количество правильных ответов за одну игру) за всё время и дата, когда этот рекорд был установлен. Эти данные должны сохраняться. После каждой игры текущий результат сравнивается с сохранённым рекордом, и если он лучше, рекорд обновляется.
|
||||
- **Средняя точность в процентах**: Это отношение общего количества правильных ответов во всех сыгранных квизах к общему количеству вопросов во всех сыгранных квизах. Для расчёта этого значения рекомендуется хранить два отдельных накопительных счётчика: общее количество всех правильных ответов (`totalCorrectAnswers`) и общее количество всех заданных вопросов (`totalQuestionsAsked`). Эти счётчики обновляются после каждого квиза.
|
||||
|
||||
Вы добавите в `UserDefaults` данные по результатам каждой игры, а затем будете забирать данные для отображения статистики на алерте в конце раунда.
|
||||
|
||||
@@ -52,7 +59,6 @@ protocol StatisticServiceProtocol {
|
||||
|
||||
Чтобы показать пользователю лучший результат, в конце каждого раунда мы будем:
|
||||
|
||||
- запоминать количество сыгранных игр;
|
||||
- сравнивать результат текущей игры с рекордом, который хранится в `UserDefaults`;
|
||||
- обновлять рекорд, если новый результат лучше — то есть если пользователь ответил правильно на большее количество вопросов.
|
||||
|
||||
@@ -121,26 +127,26 @@ protocol StatisticServiceProtocol {
|
||||
}
|
||||
```
|
||||
|
||||
> Осталось только написать реализацию протокола и реализовать полный текст для алерта. Если вы уверены в знаниях по работе с `UserDefaults`, от этой точки можете реализовать протокол сами, а потом сравнить решение с подходом, который мы предлагаем в уроке.
|
||||
> Осталось только написать реализацию протокола и реализовать полный текст для алерта. Если вы уверены в знаниях по работе с `UserDefaults`, от этой точки можете реализовать протокол сами, а потом сравнить решение с подходом, который мы предлагаем в уроке.
|
||||
|
||||

|
||||
|
||||
## Реализуем `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-разработки.
|
||||
Reference in New Issue
Block a user