diff --git a/lessons/05.Sprint/00.Введение в спринт.md b/lessons/05.Sprint/00.Введение в спринт.md index 62a44abf..4e834fd9 100644 --- a/lessons/05.Sprint/00.Введение в спринт.md +++ b/lessons/05.Sprint/00.Введение в спринт.md @@ -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 даёт много возможностей! КНОПКА Отлично! Перейдём к первой теме diff --git a/lessons/05.Sprint/03.Хранение данных/00.Содержание.md b/lessons/05.Sprint/03.Хранение данных/00.Введение в тему.md similarity index 79% rename from lessons/05.Sprint/03.Хранение данных/00.Содержание.md rename to lessons/05.Sprint/03.Хранение данных/00.Введение в тему.md index cc913cc3..cbe5586b 100644 --- a/lessons/05.Sprint/03.Хранение данных/00.Содержание.md +++ b/lessons/05.Sprint/03.Хранение данных/00.Введение в тему.md @@ -1,4 +1,4 @@ -# Содержание +# Введение в тему Привет! Рады видеть вас в новой теме. В ней вы разберётесь, как приложение хранит разные типы данных на устройстве. @@ -20,7 +20,9 @@ Углубимся в один из основных инструментов для хранения легковесных данных на устройстве — `UserDefaults`. Вы изучите, как с помощью этого инструмента сохранять и читать примитивные типы данных: строки, числа и даты. -## Сохранение данных и статистика MovieQuiz +`UserDefaults` хорошо подходит для хранения небольших объёмов данных, примитивных типов *(числа, строки, логические)*, таких как настройки пользователя или простая статистика. Он прост в использовании и не требует никакой настройки. *Для более объёмных или структурированных данных лучше рассмотреть другие инструменты, например, Core Data (спринт 15).* + +## Сохранение данных и статистика MovieQuiz После теоретического знакомства с `UserDefaults` пора применять знания на практике. Вы разработаете сервис для сохранения и отображения статистики пользователя в приложении MovieQuiz. diff --git a/lessons/05.Sprint/03.Хранение данных/01.Sandbox и Bundle.md b/lessons/05.Sprint/03.Хранение данных/01.Sandbox и Bundle.md index 9f6193d9..90d92f05 100644 --- a/lessons/05.Sprint/03.Хранение данных/01.Sandbox и Bundle.md +++ b/lessons/05.Sprint/03.Хранение данных/01.Sandbox и Bundle.md @@ -6,7 +6,7 @@ Мы разберём в этой теме: -- что такое `Bundle` и когда он может пригодиться; +- что такое `Bundle` и когда он может пригодиться; - почему `Sandbox` – особое место в памяти девайса и чем он полезен. Понимание этих концепций поможет при организации хранения данных приложения, а ещё выделит вас на фоне других кандидатов во время технических интервью. Вопросы про `Sandbox` и `Bundle` часто встречаются в интервью для iOS-разработчиков: знания в этих областях указывают на то, что вы можете обеспечить безопасность приложения и его грамотную архитектуру. Это может стать решающим фактором при найме! diff --git a/lessons/05.Sprint/03.Хранение данных/02.Способы хранения данных на устройстве.md b/lessons/05.Sprint/03.Хранение данных/02.Способы хранения данных на устройстве.md index 6efa9e35..0e9e1c04 100644 --- a/lessons/05.Sprint/03.Хранение данных/02.Способы хранения данных на устройстве.md +++ b/lessons/05.Sprint/03.Хранение данных/02.Способы хранения данных на устройстве.md @@ -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. diff --git a/lessons/05.Sprint/03.Хранение данных/03. UserDefaults.md b/lessons/05.Sprint/03.Хранение данных/03.UserDefaults.md similarity index 84% rename from lessons/05.Sprint/03.Хранение данных/03. UserDefaults.md rename to lessons/05.Sprint/03.Хранение данных/03.UserDefaults.md index 91fdb5ce..9b66592d 100644 --- a/lessons/05.Sprint/03.Хранение данных/03. UserDefaults.md +++ b/lessons/05.Sprint/03.Хранение данных/03.UserDefaults.md @@ -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`; diff --git a/lessons/05.Sprint/03.Хранение данных/04. Сохранение данных MovieQuiz.md b/lessons/05.Sprint/03.Хранение данных/04.Сохранение данных MovieQuiz.md similarity index 76% rename from lessons/05.Sprint/03.Хранение данных/04. Сохранение данных MovieQuiz.md rename to lessons/05.Sprint/03.Хранение данных/04.Сохранение данных MovieQuiz.md index f0b1594d..b1257378 100644 --- a/lessons/05.Sprint/03.Хранение данных/04. Сохранение данных MovieQuiz.md +++ b/lessons/05.Sprint/03.Хранение данных/04.Сохранение данных MovieQuiz.md @@ -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) для вас. Вот несколько стикеров из набора: diff --git a/lessons/05.Sprint/03.Хранение данных/05.Задание по теме.md b/lessons/05.Sprint/03.Хранение данных/05.Задание по теме.md new file mode 100644 index 00000000..03133f0f --- /dev/null +++ b/lessons/05.Sprint/03.Хранение данных/05.Задание по теме.md @@ -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`. diff --git a/lessons/05.Sprint/Итоги спринта.md b/lessons/05.Sprint/Итоги спринта.md new file mode 100644 index 00000000..faa0282f --- /dev/null +++ b/lessons/05.Sprint/Итоги спринта.md @@ -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-разработки.