Гид по проекту: Личностный тест

Гид по проекту: Личностный тест

В этом разделе вы узнали о механизмах, предоставляемых платформой UIKit для управления потоком вашего приложения. Ранее вы узнали, как управлять положением и размером видов и элементов управления с помощью автоматической компоновки и стековых видов. Теперь вы объедините эти знания для создания приложения.

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

  • Что ты за животное?
  • Какую страну вам следует посетить в следующий раз?
  • Каким продуктом Apple вы являетесь?

На вопросы викторины нет правильных ответов. Ответы носят чисто субъективный характер, и их результаты даже не обязательно должны быть логичными. Например, предположим, что вы разрабатываете тест под названием “Какую страну вам следует посетить в следующий раз?” Вы могли бы задать вопрос “Какой ваш любимый цвет?” и решить, что ответ “зеленый” соответствует Италии, а не Франции. Другие вопросы и ответы могут иметь больше смысла. Если игрок предпочитает суши пасте, вы можете начислять очки за Японию, а не за Италию.

В этом проекте с гидом в качестве темы викторины будет использоваться вопрос “Какое ты животное?”. Возможны четыре исхода: собака, кошка, кролик и черепаха. Но если вы предпочитаете выбирать свою собственную тему и результаты, продолжайте. Пока вы следуете инструкциям в проекте, подойдет любая тема. Вы узнаете больше, если хорошо проведете время. 

Часть первая

Планирование проекта

Прежде чем погружаться непосредственно в Interface Builder или Swift, важно подумать о целях и требованиях вашего теста на личность. Кто ваша целевая аудитория? Возможно, у вас уже есть тема, но какие функции будет включать тест? Какие модели и представления вам понадобятся для реализации этих функций?

Если вы попытаетесь начать писать код, не обдумав эти вопросы, вы, вероятно, в конечном итоге перепишете или выбросите много работы. Потратьте некоторое время сейчас, чтобы спланировать свой проект и обдумать наилучший подход к нему.

Функции

Какие функции необходимы для создания увлекательного теста на личность? Поскольку это, вероятно, ваше первое приложение такого типа, держите список коротким. Минимум, что должно делать приложение:

  • Представить игроку тест.
  • Показать вопросы и ответы.
  • Отобразить результаты.

Рабочий процесс

С учетом этих трех целей попробуйте представить ваше приложение как серию связанных представлений. Каждая функция очень отличается от других двух, поэтому каждая заслуживает своего собственного экрана. Какой-то тип ввода будет перемещать игрока от одной функции к следующей. Например, вы можете включить кнопку "Начать тест", чтобы переместить игрока от введения к вопросам, а ответ на последний вопрос может перейти к отображению результатов. Для целей этого проекта вам понадобится как минимум три контроллера представлений — по одному для каждой из трех функций.

Вы также думали о представлении нового контроллера для каждого вопроса? Это на самом деле не обязательно. В предыдущих уроках вы узнали о двух методах представления контроллеров — модально или с помощью push-перехода в навигационный стек. Модально представленный экран обычно включает способ его закрытия, и любой новый контроллер, который вы добавляете в навигационный контроллер, имеет кнопку "Назад" в верхнем левом углу. В любом случае всегда подразумевается метод закрытия.

Что бы произошло, если бы у вас были отдельные контроллеры представлений для каждого вопроса? Представьте, что игрок находится на девятом вопросе и хочет вернуться к третьему вопросу. Если бы у вас был контроллер представлений для каждого вопроса, ему пришлось бы закрыть шесть контроллеров, чтобы вернуться назад. Это возможно, но тогда ему пришлось бы снова отвечать на все промежуточные вопросы, чтобы вернуться к девятому вопросу.

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

Представления

В зависимости от темы вашего теста и вопросов, которые вы хотите задать, вашему тесту на личность могут потребоваться разные элементы управления вводом. Рассмотрите следующие вопросы:

  • Какую еду вы любите больше всего?
  • Какую из следующих продуктов вы любите?
  • Насколько вы любите этот конкретный продукт?

Первый вопрос является вопросом с множественным выбором, где только один ответ допустим. Для этого вопроса вы могли бы использовать кнопку для каждого продукта. Второй вопрос может иметь ноль или более ответов. Вы могли бы использовать переключатели, чтобы игрок мог выбрать столько продуктов, сколько ему нравится, а также кнопку для отправки своих выборов. Третий вопрос может включать шкалу от 1 до 10, используя ползунок.

Как видите, тип вопроса диктует, какие элементы управления будут отображаться на экране. С одним контроллером представлений для всех вопросов и ответов в вашем тесте вы захотите отображать только те кнопки, переключатели, ползунок и/или метки, которые соответствуют текущему вопросу. Самый простой и лучший способ сделать это — сгруппировать элементы управления в пределах представления, соответствующего типу вопроса. Соответствующее представление — для одного ответа, нескольких ответов или диапазонного ответа — затем можно показать или скрыть.

Модели

Какой тип данных вам нужен для теста на личность? Сначала вы могли бы подумать о создании массива строк для хранения ваших вопросов, но где бы вы хранили ответы? Лучше создать структуру Question, которая содержит коллекцию ответов. Коллекция ответов также должна быть больше, чем массив строк, потому что каждый ответ будет соответствовать одному из результатов теста. По крайней мере, вам потребуется включить структуру Question, структуру Answer и некоторый тип результата.

Рассмотрим тип результата. В тесте на личность ответ может соответствовать только одному результату. Например, в тесте о животных результат никогда не будет одновременно собакой и кошкой. Это будет либо одно, либо другое. Это идеальный случай для использования перечисления (enumeration). Так же, как перечисление Direction может иметь случаи north, south, east и west, AnimalType может быть dog, cat, rabbit или turtle. Чтобы ознакомиться с деталями использования перечисления enum, посетите урок по Enumerations, представленный ранее в этом разделе.

Быстрый обзор

Теперь, когда вы проанализировали, какие компоненты вам понадобятся, вероятно, легче понять, как будет собираться проект. Вы будете использовать три контроллера представлений для вашего теста:

  • Первый контроллер представлений — это вводный экран с информацией о тесте и кнопкой для начала.
  • Второй контроллер представлений отображает вопрос и несколько ответов, а также управляет ответами. Этот контроллер обновляется для каждого вопроса, и в зависимости от типа задаваемого вопроса будут отображаться соответствующие элементы управления.
  • Третий контроллер представлений подводит итоги ответов и представляет окончательный результат. Этот результат можно закрыть, позволяя другому игроку начать тест с первого контроллера представлений.

 

Часть вторая

Настройка проекта

Создайте новый проект, используя шаблон iOS App. Назовите его "PersonalityQuiz" и откройте основной storyboard. Storyboard уже содержит один контроллер представлений, но вам понадобятся еще два. Перетащите два контроллера представлений из библиотеки объектов на холст и расположите все три в горизонтальном ряду. 1 

 

 

Создание вводного экрана

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

В качестве примера взгляните на вводный экран ниже, а затем следуйте инструкциям, чтобы узнать, как его создать. Вы можете использовать тот же экран или что-то похожее, главное, чтобы тема теста была ясна для игрока.

Добавьте метку из библиотеки объектов на контроллер представлений. Затем добавьте кнопку прямо под меткой. Выберите метку и используйте инспектор атрибутов, чтобы установить выравнивание по центру. Теперь измените текст метки, цвет текста и шрифт. На этом скриншоте текст гласит "Which Animal Are You?" с использованием шрифта Georgia Regular 30.0. 2 

Выберите кнопку и обновите заголовок, чтобы он гласил "Begin Personality Quiz", с использованием шрифта System 15.0. 3 

Когда у вас есть несколько элементов в горизонтальном ряду или вертикальной колонке, хорошей идеей будет использовать stack view. Этот подход уменьшит количество ограничений, которые вам нужно будет создать и управлять ими.

Выделите метку и кнопку, затем нажмите кнопку Embed In и выберите Stack View. 4 

С выделенным stack view используйте инспектор атрибутов, чтобы убедиться, что Axis установлен на Vertical, а Alignment и Distribution установлены на Fill. Эти настройки обеспечат вертикальное расположение элементов в стеке и заполнение всего доступного пространства вдоль оси stack view.

Метка и кнопка должны быть центрированы как по горизонтали, так и по вертикали. Хотя вы можете создать два ограничения для этого, метка находится слишком близко к горизонтальным границам экрана. Используйте leading и trailing ограничения, чтобы убедиться, что она не выходит за границы экрана независимо от размера устройства.

Используйте инструмент Add Constraint, чтобы создать leading и trailing ограничения на расстоянии 8 пунктов от каждой стороны. 5 

Далее используйте инструмент Align, чтобы добавить ограничение, которое центрирует stack view по вертикали. Нажмите "Add 1 Constraint". 6 

Это все, что необходимо для вводного экрана, но это немного скучно. Каковы возможные результаты? Для этой темы было бы весело добавить эмодзи для каждого животного (собака, кошка, кролик и черепаха) и разместить их в четырех углах представления. 7  Если нет эмодзи, подходящих для вашей темы теста, рассмотрите возможность использования изображений вместо текста с эмодзи.

Если вы еще этого не сделали, перетащите еще четыре метки из библиотеки объектов на представление. Замените текст на эмодзи для каждого животного. Чтобы вызвать выбор эмодзи, выделите текст метки в инспекторе атрибутов, затем нажмите Control-Command-Space. Увеличьте все эмодзи, установив шрифт на System 40.0. Наконец, используйте синие направляющие для размещения каждой метки в углу с рекомендованными отступами.

Чтобы удерживать ваши эмодзи в соответствующих углах на всех размерах экрана, вам нужно добавить два ограничения для каждой метки. Начните с выбора метки в верхнем левом углу и нажмите кнопку Add New Constraints. Включите верхнее и ведущее ограничения. Если вы использовали направляющие, верхнее ограничение должно иметь значение 0, а ведущее ограничение — 20. Добавьте эти два ограничения.

Позиция вашего эмодзи в верхнем левом углу готова. Теперь повторите шаги для остальных трех меток, используя соответствующие края для создания ограничений. Проверьте значения четырех инструментов Add New Constraints, слева направо на следующей диаграмме, для меток собаки, кошки, кролика и черепахи.

Xcode не удовлетворен только этими ограничениями. Он выдает предупреждение, уведомляющее, что отсутствуют trailing и leading ограничения, что может вызвать наложение на другие представления. Причина в том, что метки часто имеют динамические значения или размеры. Для этого дизайна интерфейса можно оставить все как есть. Однако, для лучшей практики следует устранить все предупреждения компилятора.

Для элементов 🐶 и 🐰 необходимы trailing ограничения, а для 🐱 и 🐢 — leading ограничения. Вы можете использовать несколько вариантов для удовлетворения требований вашего дизайна интерфейса. Самый быстрый — добавить горизонтальное пространство между 🐶 и 🐱 а также между 🐰 и 🐢. Установите ограничения на ≥ 0, чтобы они оставались на месте независимо от размера экрана.

Нажмите Control и перетащите от 🐶 к 🐱 и выберите Horizontal Spacing. 8 

С добавленным ограничением перейдите к инспектору размеров и найдите ограничение “Trailing Space to: 🐱”. Нажмите "Edit" и установите "Constant" на ≥ 0. 9  Повторите процесс для 🐰 и 🐢. По ходу дела, после позиционирования и добавления ограничений, вы можете заметить некоторые желтые предупреждения. Это нормально. Нажмите кнопку "Update Frames"  внизу Interface Builder, чтобы скорректировать положение и размер ваших представлений в соответствии с созданными ограничениями.

Создание экрана вопросов и ответов

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

После того как игрок ответит на вопрос, ваше приложение должно принять решение:

  • Если в тесте есть еще один вопрос, обновите метки и элементы управления в контроллере представлений соответственно.
  • Если вопросов больше нет, отобразите результаты в новом контроллере представлений.

Как приложение будет знать, что делать? Вам нужно создать логику, которая определяет, нужно ли выполнять переход после получения ответа на текущий вопрос. Если вы создадите переход, перетащив зажав Control от элемента управления вводом к следующему контроллеру представлений, это приведет к выполнению перехода всякий раз, когда игрок взаимодействует с элементом управления.

Вместо этого вы можете программно вызвать переход между вторым и третьим контроллерами представлений. Перетащите зажав Control от значка контроллера представлений, расположенного над представлением второго контроллера, к третьему контроллеру представлений 10  и создайте переход Show. Выделите переход на storyboard. Затем используйте инспектор атрибутов, чтобы присвоить ему идентификаторную строку "Results".

Как вы уже узнали, когда вы встраиваете корневой контроллер представлений в навигационный контроллер, переход Show изменяется с модального представления на push-переход справа налево. В вашем тесте, когда игрок ответит на последний вопрос, вы можете выполнить push-переход к финальному контроллеру представлений, чтобы отобразить результаты.

Должны ли все три контроллера представлений находиться в навигационном контроллере? Помните, что модальное представление является правильным выбором всякий раз, когда контекст вашего приложения меняется. И это смена контекста, когда игрок переходит с экрана введения на экран вопросов. Это означает, что первый контроллер представлений должен модально представлять контроллер представлений, содержащийся в навигационном контроллере. Выберите второй контроллер представлений, затем нажмите кнопку Embed In и выберите Navigation Controller.

Создайте переход Show от кнопки первого контроллера представлений к навигационному контроллеру. Это даст пользователю возможность начать тест.

Создание экрана результатов

После того, как игрок завершит тест, приложение отобразит результаты вместе с кратким описанием. У игрока должна быть возможность закрыть результаты и вернуться на экран введения, чтобы другой игрок мог пройти тест. Вот пример такого интерфейса:

Теперь, когда вы встроили экран результатов в навигационный контроллер, появилась навигационная панель для размещения заголовков и кнопок — при условии, что контроллер представлений имеет элемент навигации. Сейчас вы добавите этот элемент.

Обновите заголовок финального контроллера представлений на "Results" в инспекторе атрибутов для его элемента навигации или просто дважды щелкните по элементу навигации. 11 

Добавьте кнопку "Done", которая закрывает результаты и возвращает на экран введения. Перетащите элемент кнопки панели из библиотеки объектов 12  на правую сторону элемента навигации. В инспекторе атрибутов обновите атрибут System Item кнопки панели на "Done", что автоматически изменит текст и шрифт кнопки. 13 

Теперь вы уже профессионал в размещении stack views с ограничениями. Чтобы воссоздать экран результатов, создайте вертикальный stack view с двумя метками. Используйте инструмент Align, чтобы центрировать stack по вертикали, и используйте инструмент Add New Constraints, чтобы установить отступы от ведущего и замыкающего краев на 20. 14  Используйте шрифт System 50.0 для первой метки и System 17.0 для второй метки. Установите выравнивание для обеих меток по центру. Вторая метка будет описывать результаты более подробно и, вероятно, будет содержать несколько строк, поэтому установите атрибут Lines на 0 и Line Break на Word Wrap. Ваша сцена должна состоять из двух центрированных меток, причем верхняя будет больше, чем нижняя. 15 

Создание описательных подклассов

Теперь, когда у вас есть три контроллера представлений в вашей сцене storyboard, вам понадобятся три подкласса UIViewController в коде. Создайте новый файл, выбрав File > New > File в строке меню Xcode. Выберите Cocoa Touch Class в качестве стартового шаблона, затем выберите UIViewController из всплывающего меню Subclass. Этот выбор автоматически добавит “ViewController” к имени класса, делая тип объекта понятным для других разработчиков. Назовите класс “QuestionViewController” и нажмите Next. В выпадающем меню Group должна быть указана папка, соответствующая названию проекта, PersonalityQuiz. Выберите ее и нажмите Create.

Повторите эти шаги для создания второго класса, назвав его “ResultsViewController”. Когда вы закончите, вы увидите два новых файла в Project navigator для вашего теста. 16 

Вы также можете заметить, что в Project navigator указан подкласс UIViewController под названием “ViewController.” Шаблон iOS App автоматически присвоил это имя первому контроллеру представлений вашего приложения. Чтобы сделать название более описательным, щелкните имя файла и измените его на "IntroductionViewController." Затем откройте файл и измените имя класса на "IntroductionViewController," после чего закройте файл.

Теперь в вашем проекте есть три подкласса UIViewController с описательными названиями. Снова откройте Main storyboard. По очереди выберите каждый контроллер представлений и используйте инспектор идентичности (Identity inspector), чтобы присвоить ему соответствующий пользовательский класс. Первый контроллер представлений будет IntroductionViewController, затем QuestionViewController и ResultsViewController.

Часть третья

Создание вопросов и ответов

Во время фазы планирования проекта вы рассмотрели три различных типа вопросов: с одним ответом, с несколькими ответами и с диапазонным ответом. Теперь уделите время составлению списка ваших вопросов. Подумайте о том, как можно переформулировать каждый вопрос, чтобы он соответствовал одной из трех категорий.

Вопросы с одним ответом

Допустим, вы спрашиваете: "Какая еда вам нравится больше всего?" Ответ может включать список из четырех видов еды, и игрок должен выбрать один. Какой тип элемента управления вы бы использовали? Простой подход — представить кнопку для каждого ответа, организованную в вертикальном stack view.

Начните с перетаскивания вертикального stack view из библиотеки объектов в QuestionViewController. Теперь добавьте четыре кнопки в stack view. Используйте инструмент Align, чтобы центрировать stack по вертикали внутри представления, затем используйте инструмент Add New Constraints, чтобы установить его ведущие и замыкающие края на 20 пикселей. Добавьте пространство между кнопками, установив значение Spacing на 20 в инспекторе атрибутов. Если необходимо, нажмите кнопку Update Frames, чтобы перепозиционировать stack на основе созданных вами ограничений. 17 

Конечно, заголовки кнопок будут меняться в зависимости от предоставленных ответов. Вы обновите их позже, когда перейдете к программированию викторины.

Вопросы с несколькими ответами

Вопрос "Какие из следующих продуктов вам нравятся?" предполагает, что игрок может выбрать несколько ответов. Вместо использования кнопок для ответов, логичнее создать пары меток и переключателей, чтобы игрок мог включить все положительные ответы. Когда игрок сделает свой выбор, он может нажать кнопку для отправки ответов и перехода к следующему вопросу.

Перед началом работы вы можете скрыть stack view с одним ответом. Выберите stack на storyboard или в outline, затем откройте инспектор атрибутов и снимите галочку с Installed в нижней части панели.

Интерфейс с переключателями не сильно отличается от интерфейса с кнопками. Каждая пара метка-переключатель может быть помещена в горизонтальный stack view. И, как и в вопросе с одним ответом, строки могут быть помещены в вертикальный stack view.

Начните с добавления метки и переключателя из библиотеки объектов. Выделите их оба, затем нажмите кнопку Embed In и выберите Stack View. В инспекторе атрибутов для stack view убедитесь, что Axis установлен на Horizontal, а Alignment и Distribution установлены на Fill. 18 

Выберите stack view, затем скопируйте (Command-C) и вставьте (Command-V), чтобы добавить три копии на представление. Теперь выберите все четыре горизонтальных стека и нажмите кнопку Embed In, затем выберите Stack View, чтобы поместить их в другой stack view. В инспекторе атрибутов для этого нового stack view установите Axis на Vertical, Alignment и Distribution на Fill, а также установите значение Spacing между элементами на 20. 19 

Добавьте кнопку в нижнюю часть stack view и установите ее заголовок на “Submit Answer.” Наконец, используйте инструмент Align, чтобы центрировать stack по вертикали внутри представления, затем используйте инструмент Add New Constraints, чтобы установить отступы ведущих и замыкающих краев на 20 пикселей от каждого края. 20  Если необходимо, используйте кнопку Update Frames, чтобы перепозиционировать рамки на основе созданных вами ограничений. 21 

Вопросы с диапазоном ответов

Третий тип вопроса может выглядеть так: "Насколько вам нравится эта еда?" Вы, вероятно, могли бы придумать способ использовать кнопку или переключатель для ответа, но игрок может получить лучшее впечатление, если его выбор будет более свободным. Чтобы дать игроку возможность выбора из диапазона ответов, вы можете использовать ползунок в качестве элемента управления вводом, с меткой на каждом конце ползунка.

Чтобы упростить задачу, вы можете скрыть stack view для нескольких ответов, как вы это сделали ранее с stack view для одного ответа. Выберите его в storyboard, откройте инспектор атрибутов и снимите галочку с Installed в нижней части панели.

Вы можете использовать stack view для создания этого интерфейса без необходимости определения множества ограничений — аналогично подходу с переключателями. Начните с добавления двух меток на холст из библиотеки объектов, затем выделите обе, нажмите кнопку Embed In и выберите Stack View. В инспекторе атрибутов убедитесь, что Axis установлен на Horizontal, Alignment — на Fill, а Distribution — на Equal Spacing. 22 

Далее перетащите ползунок из библиотеки объектов на холст. Выберите ползунок и горизонтальный стек, затем нажмите кнопку Embed In и выберите Stack View. В инспекторе атрибутов убедитесь, что Axis установлен на Vertical, Alignment и Distribution установлены на Fill, а Spacing установлено на 20. 23 

Добавьте кнопку в нижнюю часть стека и установите ее заголовок на “Submit Answer.” Используйте инструмент Align, чтобы центрировать стек по вертикали внутри представления, затем используйте инструмент Add New Constraints, чтобы выровнять ведущие и замыкающие края с отступом 20 пикселей от каждого края. 24  Если необходимо, используйте кнопку Update Frames, чтобы перепозиционировать рамки на основе созданных вами ограничений. 25 

Перед тем как продолжить, вам нужно будет снова включить stack views, которые вы отключили во время процесса создания. В Document Outline выберите каждый stack view, затем выберите галочку Installed в инспекторе атрибутов.

Метка вопроса и прогресс

Независимо от того, какой вопрос вы задаете своим игрокам, вам нужно отобразить его в метке. Добавьте метку в верхнюю часть представления. Используйте инструмент Add New Constraints, чтобы разместить метку на 20 пикселей ниже навигационной панели и на 20 пикселей от ведущих и замыкающих краев. 26  В инспекторе атрибутов установите выравнивание текста по центру и шрифт на System Font 32.0. Установите атрибут Lines на 0, чтобы метка могла использовать столько строк, сколько нужно. Измените атрибут Line Break на Word Wrap. 27 

Игрокам часто нравится знать, насколько далеко они продвинулись в викторине. Найдите “Progress View” в библиотеке объектов и добавьте его в представление. Используйте инструмент Add New Constraints, чтобы разместить прогресс-бар на 20 пикселей от нижнего края и на 20 пикселей от ведущих и замыкающих краев представления. 28 

Часть четвертая

Модели и аутлеты

До сих пор в этом уроке вы спроектировали контроллеры представлений в storyboard и подготовили три подкласса UIViewController для приема кода. Теперь пришло время создать структуры, которые будут содержать данные вопросов, и обновить пользовательский интерфейс на основе значений каждого вопроса и его ответов. Как только данные будут разложены, вы сможете обновить пользовательский интерфейс в зависимости от отображаемого вопроса.

Модели данных

Создайте новый файл под названием "Question" для размещения определений моделей. Используйте этот файл для определения всех структур, необходимых для вашей викторины. Вы можете создать этот файл, выбрав File > New > File (или Command-N) в строке меню Xcode, затем выбрав "Swift file".

Можно с уверенностью сказать, что каждый Question будет иметь текст, представляющий сам вопрос, а также массив объектов Answer. Поскольку в вашей викторине можно использовать три разных типа методов ввода, вы создадите enum, который описывает тип ответа на вопрос: одиночный ответ, множественный ответ или ответ с диапазоном. Пример структуры показан ниже:

 

struct Question {
    var text: String
    var type: ResponseType
    var answers: [Answer]
}
 
enum ResponseType {
    case single, multiple, ranged
}

 

Каждый ответ соответствует определенному типу результата. В примере с животными, предположим, что вы спрашиваете: "Какая из этих продуктов вам нравится больше всего?" и возможные ответы: "Стейк", "Рыба", "Морковь" и "Кукуруза". Каждый ответ соответствует собаки, кошки, кролику и черепахе соответственно — и, следовательно, определенному эмодзи. Если бы свойство ответов answers было массивом строк, не было бы простого способа связать ответ с конкретным результатом. Вместо этого структура Answer будет иметь строку для отображения игроку и свойство type, связывающее ответ с определенным результатом.

Вот пример данных:

 

struct Answer {
    var text: String
    var type: AnimalType
}
 
enum AnimalType: Character {
    case dog = “🐶”, cat = “🐱”, rabbit = “🐰”, turtle = “🐢”
}

 

Обычно в конце теста на определение личности игрок получает текст о результате теста. Поскольку вы уже определили enum для представления каждого типа личности — или, в данном случае, типа животного — вы могли бы включить свойство definition, которое будет отображаться в виде метки на экране результатов.

Вот пример определения для типов животных:

 

enum AnimalType: Character {
    case dog = “🐶”, cat = “🐱”, rabbit = “🐰”, turtle = “🐢”
 
    var definition: String {
        switch self {
        case .dog:
            return “You are incredibly outgoing. You surround yourself with the people you love and enjoy activities with friends.”
        case .cat:
            return “Mischievous, yet mild-tempered, you enjoy doing things on your own terms.”
        case .rabbit:
            return “You love everything that’s soft. You are healthy and full of energy.”
        case .turtle:
            return “You are wise beyond your years, and you focus on the details. Slow and steady wins the race.”
        }
    }
}

 

Отображение вопросов и ответов

QuestionViewController будет содержать массив объектов Question в свойстве под названием questions. При создании этих объектов вам нужно будет уделить особое внимание тому, сколько объектов Answer вы помещаете в свойство answers. Когда вы создавали stack views для ответов с одним и несколькими вариантами ответов, вы создавали четыре кнопки и четыре переключателя, чтобы представить фиксированное количество возможных ответов. Поэтому любой Question, который вы создаете с типом single или multiple, должен иметь ровно четыре объекта Answer.

Для диапазонного ответа вы можете обойтись всего двумя ответами: два конца ползунка. Но лучше определить четыре возможных диапазона, чтобы вопрос мог начислять очки за каждый из четырех возможных результатов. Коллекция ответов для диапазонного ответа должна быть в каком-то порядке — например, от наименее вероятного до наиболее вероятного — чтобы вы могли точно присвоить ответы результату.

В следующем примере массив заполнен вопросами каждого типа ответов: с одним ответом, с несколькими ответами и с диапазонным ответом:

 

var questions: [Question] = [
  Question(
    text: "Which food do you like the most?",
    type: .single,
    answers: [
      Answer(text: "Steak", type: .dog),
      Answer(text: "Fish", type: .cat),
      Answer(text: "Carrots", type: .rabbit),
      Answer(text: "Corn", type: .turtle)
    ]
  ),
 
  Question(
    text: "Which activities do you enjoy?",
    type: .multiple,
    answers: [
      Answer(text: "Swimming", type: .turtle),
      Answer(text: "Sleeping", type: .cat),
      Answer(text: "Cuddling", type: .rabbit),
      Answer(text: "Eating", type: .dog)
    ]
  ),
 
  Question(
    text: "How much do you enjoy car rides?",
    type: .ranged,
    answers: [
      Answer(text: "I dislike them", type: .cat),
      Answer(text: "I get a little nervous", type: .rabbit),
      Answer(text: "I barely notice them", type: .turtle),
      Answer(text: "I love them", type: .dog)
    ]
  )
]

 

Отображение вопросов с правильными элементами управления

Теперь, когда у вас есть список вопросов, из которого можно выбирать, вам нужно отслеживать, какие из них ваше приложение уже показало, и вычислять, когда все они были показаны. Один из способов — использовать целое число в качестве индекса в коллекции вопросов questions. Это целое число будет начинаться с 0 (индекс первого элемента в коллекции), и вы будете увеличивать его значение на 1 после того, как игрок ответит на каждый вопрос.

Добавьте свойство под названием questionIndex в ваш QuestionViewController:

var questionIndex = 0

Когда игрок переходит от вопроса к вопросу, вам нужно будет показывать правильный stack view и скрывать другие два. Но прежде чем вы сможете написать код, который изменяет видимость stack view, вам нужно создать необходимые аутлеты и действия.

Откройте Main storyboard и выберите QuestionViewController. Откройте помощник редактора (assistant editor), чтобы просматривать QuestionViewController рядом с storyboard. Перетащите с Control single-answer stack view в определение класса QuestionViewController, 28  затем отпустите мышь или трекпад, чтобы вызвать всплывающее окно. Убедитесь, что тип соединения установлен на Outlet, затем введите “singleStackView” в поле Name и нажмите Connect. Повторите эти шаги еще два раза, введя имена multipleStackView для стека с несколькими ответами и rangedStackView для стека с диапазоном.

Затем создайте многоразовый метод под названием updateUI(), который вы сможете вызывать перед отображением каждого вопроса игроку. Вы должны вызвать этот метод в viewDidLoad(), чтобы установить правильный интерфейс для первого вопроса.

 

override func viewDidLoad() {
    super.viewDidLoad()
    updateUI()
}
 
func updateUI() {
 
}

 

Метод updateUI() отвечает за обновление нескольких ключевых элементов интерфейса, включая заголовок в навигационной панели и видимость stack views. Вы можете использовать свойство questionIndex для создания уникального заголовка — например, "Question #4" — в навигационном элементе для каждого вопроса. С помощью stack views проще всего скрыть все три stack views, затем проверить свойство type объекта Question, чтобы определить, какой stack должен быть видимым.

Вы можете использовать свойство questionIndex в сочетании с коллекцией questions для доступа к конкретному вопросу:

 

func updateUI() {
    singleStackView.isHidden = true
    multipleStackView.isHidden = true
    rangedStackView.isHidden = true
 
    navigationItem.title = “Question #\(questionIndex + 1)”
 
    let currentQuestion = questions[questionIndex]
 
    switch currentQuestion.type {
    case .single:
        singleStackView.isHidden = false
    case .multiple:
        multipleStackView.isHidden = false
    case .ranged:
        rangedStackView.isHidden = false
    }
}

 

Соберите и запустите ваше приложение. Если вы все настроили правильно, видимый stack view должен соответствовать первому вопросу, который вы определили в свойстве questions. Попробуйте изменить порядок вопросов, чтобы протестировать каждый интерфейс.

Обновление заголовков кнопок и текста меток

Интерфейс на вашем экране вопросов работает, но вам все еще нужно обновить заголовки кнопок и текст меток. Для этого вам нужно создать аутлеты для меток и кнопок, связанных с каждым stack view.

В дополнение к аутлетам, которые вы создали для stack views, этот экран требует 12 аутлетов для элементов управления и меток. В single-answer stack view есть четыре аутлета для кнопок, в multiple-answer stack view — четыре аутлета для меток, и в ranged response stack view — два аутлета для меток. У вас также есть метка, которая отображает текст вопроса в верхней части экрана, и progress view в нижней части.

Когда вы создаете большое количество аутлетов, важно использовать краткие и легко узнаваемые имена переменных, а также держать объявления переменных организованными рядом с их соответствующими аутлетами stack view. Пример хороших имен переменных и организации аутлетов приведен ниже:

 

@IBOutlet var questionLabel: UILabel!
 
@IBOutlet var singleStackView: UIStackView!
@IBOutlet var singleButton1: UIButton!
@IBOutlet var singleButton2: UIButton!
@IBOutlet var singleButton3: UIButton!
@IBOutlet var singleButton4: UIButton!
 
 
@IBOutlet var multipleStackView: UIStackView!
@IBOutlet var multiLabel1: UILabel!
@IBOutlet var multiLabel2: UILabel!
@IBOutlet var multiLabel3: UILabel!
@IBOutlet var multiLabel4: UILabel!
 
 
@IBOutlet var rangedStackView: UIStackView!
@IBOutlet var rangedLabel1: UILabel!
@IBOutlet var rangedLabel2: UILabel!
 
@IBOutlet var questionProgressView: UIProgressView!

 

Создайте указанные выше аутлеты. Поскольку на экране так много элементов управления, перекрывающих друг друга, вы, вероятно, найдете проще перетаскивать с помощью Control-drag из элемента в Document Outline в определение контроллера представления, а не перетаскивать с холста.

После создания аутлетов вы можете обновить каждый из элементов управления в методе updateUI(). Два из аутлетов, questionLabel и progressView, нужно обновлять с каждым новым вопросом. Аутлеты для меток и кнопок нужно обновлять только в том случае, если их связанный stack view будет отображаться.

Для метки вопроса назначьте ее текст текущей строке вопроса. Для индикатора прогресса вычислите процент завершения, разделив questionIndex на общее количество вопросов.

 

func updateUI() {
    singleStackView.isHidden = true
    multipleStackView.isHidden = true
    rangedStackView.isHidden = true
 
    let currentQuestion = questions[questionIndex]
    let currentAnswers = currentQuestion.answers
    let totalProgress = Float(questionIndex) / Float(questions.count)
 
    navigationItem.title = “Question #\(questionIndex + 1)”
    questionLabel.text = currentQuestion.text
    questionProgressView.setProgress(totalProgress, animated: true)
 
    switch currentQuestion.type {
    case .single:
        singleStackView.isHidden = false
    case .multiple:
        multipleStackView.isHidden = false
    case .ranged:
        rangedStackView.isHidden = false
    }
}

 

Чтобы сделать оператор switch более кратким, вы можете вынести обновления элементов управления, специфичных для каждого стека, в отдельные методы.

В stack view для одного ответа каждая кнопка соответствует ответу. Используйте метод setTitle(_:for:) для обновления заголовка. Первая кнопка будет использовать первую строку ответа, вторая кнопка будет использовать вторую строку ответа и так далее.

 

func updateSingleStack(using answers: [Answer]) {
    singleStackView.isHidden = false
    singleButton1.setTitle(answers[0].text, for: .normal)
    singleButton2.setTitle(answers[1].text, for: .normal)
    singleButton3.setTitle(answers[2].text, for: .normal)
    singleButton4.setTitle(answers[3].text, for: .normal)
}

 

Аналогично, в stack view для нескольких ответов текст каждой метки соответствует ответу. Установите свойство text первой метки на первую строку ответа и повторите это для остальных трех меток.

 

func updateMultipleStack(using answers: [Answer]) {
    multipleStackView.isHidden = false
    multiLabel1.text = answers[0].text
    multiLabel2.text = answers[1].text
    multiLabel3.text = answers[2].text
    multiLabel4.text = answers[3].text
}

 

Для диапазонного ответа вам нужно настроить stack view немного по-другому. Хотя обновлять нужно только две метки, викторина будет работать лучше, если у каждого вопроса будет четыре ответа (даже если требуются только два ответа).

Поскольку количество ответов не гарантировано, небезопасно напрямую индексировать коллекцию. Например, если вы используете answers[3] для доступа к четвертому элементу в массиве answers, но коллекция содержит только две структуры Answer, программа завершится аварийно.

Независимо от того, сколько у вас ответов на вопрос с диапазоном, свойства first и last коллекции позволяют безопасно получить доступ к двум структурам Answer, которые соответствуют меткам.

 

func updateRangedStack(using answers: [Answer]) {
    rangedStackView.isHidden = false
    rangedLabel1.text = answers.first?.text
    rangedLabel2.text = answers.last?.text
}

 

С этими тремя новыми методами обновите случаи оператора switch в методе updateUI() для их вызова.

 

switch currentQuestion.type {
case .single:
    updateSingleStack(using: currentAnswers)
case .multiple:
    updateMultipleStack(using: currentAnswers)
case .ranged:
    updateRangedStack(using: currentAnswers)
}

 

Соберите и запустите ваше приложение. Метки и кнопки должны обновиться, чтобы отразить первый вопрос. Отличная работа! Если вы видите элементы управления, которые не обновляются, используйте инспектор соединений для QuestionViewController, чтобы убедиться, что вы правильно создали каждый @IBOutlet. Наведите курсор на каждый элемент в списке, чтобы проверить их аутлеты, и—если необходимо—разорвите любые неправильные аутлеты.

Получение ответов с помощью действий

До сих пор вы создали список вопросов, и ваше приложение отображает правильный интерфейс для первого вопроса. Это отличный старт. В этом разделе вы будете записывать ответы игрока и переходить к следующему вопросу. Когда игрок ответит на все вопросы в коллекции, вы отобразите экран результатов.

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

 

var answersChosen: [Answer] = []

 

В stack view для одного ответа вы определите, какому результату соответствует каждая нажатая кнопка, добавите его в коллекцию, а затем перейдете к следующему вопросу. Все четыре кнопки будут выполнять одинаковую работу, поэтому вы можете создать один @IBAction, который будет вызываться любой из четырех кнопок при нажатии.

Начните с перетаскивания с помощью Control-drag из первой кнопки в stack view для одного ответа в область внутри определения класса QuestionViewController. 29  Это делается так же, как вы создавали @IBOutlet, но на этот раз измените тип соединения на Action. Назовите метод “singleAnswerButtonPressed” и измените тип с Any на UIButton, чтобы параметр sender метода был типа UIButton.

Свяжите три оставшиеся кнопки в stack view для одного ответа с этим недавно созданным @IBAction. 30 

Почему нужно обновить тип? Вы связываете нажатие нескольких кнопок с этим одним действием, поэтому нужно указать, какая кнопка вызвала метод. Вы можете использовать оператор if и == для сравнения двух объектов UIButton, или вы можете использовать оператор switch. Если метод был вызван с использованием singleButton1, приложение узнает, что игрок выбрал первый ответ.

Попробуйте заполнить тело действия кодом, который проверяет, какая кнопка была нажата, затем добавляет выбранный ответ в массив answersChosen. После добавления ответа в массив вам нужно будет перейти к следующему вопросу. Создайте новый метод под названием nextQuestion() и оставьте его пустым. Вы заполните его позже. На данный момент просто добавьте вызов nextQuestion() в конце singleAnswerButtonPressed(_:)

 

@IBAction func singleAnswerButtonPressed(_ sender: UIButton) {
    let currentAnswers = questions[questionIndex].answers
 
    switch sender {
    case singleButton1:
        answersChosen.append(currentAnswers[0])
    case singleButton2:
        answersChosen.append(currentAnswers[1])
    case singleButton3:
        answersChosen.append(currentAnswers[2])
    case singleButton4:
        answersChosen.append(currentAnswers[3])
    default:
        break
    }
 
    nextQuestion()
}

 

Для интерфейса с несколькими ответами вы определите, какие ответы добавить в коллекцию на основе переключателей, которые включил игрок. Перетащите зажав Control кнопку "Submit Answer" в код и создайте действие с именем “multipleAnswerButtonPressed”. Измените атрибут Arguments на None, так как вам не нужно, чтобы кнопка определяла, какие ответы были выбраны. 31 

Далее создайте четыре аутлета, по одному для каждого UISwitch, чтобы вы могли проверить, какие из них включены, и затем добавить эти ответы в коллекцию. Перетащите зажав Control каждый UISwitch в код и дайте каждому переключателю имя. Чтобы ваш код был аккуратным и организованным, введите код для каждого из этих аутлетов рядом с переменными меток, связанными с stack view для нескольких ответов.

 

@IBOutlet var multiSwitch1: UISwitch!
@IBOutlet var multiSwitch2: UISwitch!
@IBOutlet var multiSwitch3: UISwitch!
@IBOutlet var multiSwitch4: UISwitch!

 

Если первый переключатель включен, вы хотите добавить первый ответ. В отличие от метода с вопросами с одним ответом, этот метод позволяет добавить до четырех ответов на вопрос.

 

@IBAction func multipleAnswerButtonPressed() {
    let currentAnswers = questions[questionIndex].answers
 
    if multiSwitch1.isOn {
        answersChosen.append(currentAnswers[0])
    }
    if multiSwitch2.isOn {
        answersChosen.append(currentAnswers[1])
    }
    if multiSwitch3.isOn {
        answersChosen.append(currentAnswers[2])
    }
    if multiSwitch4.isOn {
        answersChosen.append(currentAnswers[3])
    }
 
    nextQuestion()
}

 

Для вопроса с диапазоном вы будете считывать текущее положение UISlider и использовать это значение для определения, какой ответ добавить в коллекцию. Перетащите зажав Control кнопку "Submit Answer" в код и создайте действие с именем “rangedAnswerButtonPressed”. Опять же, измените атрибут Arguments на None. 32 

Далее создайте @IBOutlet для UISlider. Перетащите зажав Control ползунок из Document Outline в код и дайте ему имя. Как и на предыдущих шагах, разместите код для этого аутлета рядом с переменными меток, связанными со stack view для вопросов с диапазоном.

 

@IBOutlet var rangedSlider: UISlider!

 

Подумайте, как вы можете использовать значение ползунка для соответствия четырем различным ответам. Значение ползунка варьируется от 0 до 1, поэтому значение между 0 и 0.25 может соответствовать первому ответу, а значение между 0.75 и 1 может соответствовать последнему ответу.

Чтобы преобразовать значение ползунка в индекс массива, используйте уравнение: index = значение ползунка * (количество ответов - 1), округленное до ближайшего целого числа. Это приводит к следующей реализации метода для rangedAnswerButtonPressed:

 

@IBAction func rangedAnswerButtonPressed() {
    let currentAnswers = questions[questionIndex].answers
    let index = Int(round(rangedSlider.value *
      Float(currentAnswers.count - 1)))
 
    answersChosen.append(currentAnswers[index])
 
    nextQuestion()
}

 

Передача данных на экран результатов

Когда придет время показать экран результатов, вам нужно будет передать на него ответы пользователя. Откройте ResultsViewController и добавьте свойство для хранения ответов пользователя.

 

var responses: [Answer]

 

Это вызовет ошибку компиляции в Xcode, указывающую на то, что “Class ‘ResultsViewController’ has no initializers”. Это происходит потому, что responses не имеет начального значения и ему никогда не присваивается значение в инициализаторе. Чтобы решить эту проблему, вы создадите пользовательский инициализатор для ResultsViewController, который принимает responses в качестве аргумента, что удовлетворит компилятор. Добавьте следующий инициализатор:

 

init?(coder: NSCoder, responses: [Answer]) {
    self.responses = responses
    super.init(coder: coder)
}

 

Этот новый инициализатор принимает два параметра: coder и responses. Параметр coder будет предоставлен и используется UIKit для инициализации вашего view controller из информации, определенной в вашем Storyboard. Параметр responses будет предоставлен вами при вызове этого инициализатора и присвоен свойству self.responses, которое вы только что добавили. Наконец, вызывается инициализатор суперкласса, передавая через coder.

К сожалению, вся эта работа привела к новой ошибке компиляции, которая указывает: “required initializer init(coder:) must be provided by subclass of UIViewController.” Когда вы предоставляете свой собственный инициализатор, вы должны реализовать все требуемые инициализаторы, которые определены суперклассом.

Для получения дополнительной информации нажмите на красный символ рядом с ошибкой.

Xcode предоставляет вам "исправление" для этой проблемы. Нажмите кнопку "Fix", чтобы вставить предложенное исправление кода. 33 

 

required init?(coder: NSCoder) {
    fatalError(”init(coder:) has not been implemented”)
}

 

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

Реакция на отвеченные вопросы

Теперь, когда вы обработали ввод пользователя для каждого вопроса и можете передавать ответы в ResultsViewController, пришло время реализовать метод nextQuestion() в QuestionViewController. Для этого вы увеличиваете значение questionIndex на 1, а затем определяете, остались ли еще вопросы. Если остались, вы вызываете updateUI() для обновления заголовка и отображения соответствующего stack view. Метод использует новое значение questionIndex для отображения следующего вопроса. Если вопросов больше не осталось, пришло время представить результаты, используя segue, который вы создали в storyboard.

 

func nextQuestion() {
    questionIndex += 1
 
    if questionIndex < questions.count {
        updateUI()
    } else {
        performSegue(withIdentifier: “Results”, sender: nil)
    }
}

 

Постройте и запустите свое приложение, затем протестируйте каждый из своих элементов управления ввода. Заметили ли вы какие-либо ошибки? Одна из проблем заключается в том, что интерфейсы для вопросов с множественным выбором и с диапазоном сохраняют значения ответов с предыдущего вопроса того же типа. Например, если игрок переместил ползунок полностью влево для одного вопроса, следующий вопрос, который использует ползунок, начинается с ползунком, полностью перемещенным влево.

Чтобы решить эту проблему, вы можете сбросить позиции переключателей и ползунка в логические значения по умолчанию при отображении следующего вопроса. Обновите методы updateMultipleStack(using:) и updateRangedStack(using:), включив в них код, который сбрасывает позиции их элементов управления.

 

func updateMultipleStack(using answers: [Answer]) {
    multipleStackView.isHidden = false
    multiSwitch1.isOn = false
    multiSwitch2.isOn = false
    multiSwitch3.isOn = false
    multiSwitch4.isOn = false
    multiLabel1.text = answers[0].text
    multiLabel2.text = answers[1].text
    multiLabel3.text = answers[2].text
    multiLabel4.text = answers[3].text
}
func updateRangedStack(using answers: [Answer]) {
    rangedStackView.isHidden = false
    rangedSlider.setValue(0.5, animated: false)
    rangedLabel1.text = answers.first?.text
    rangedLabel2.text = answers.last?.text
}

 

Вторая, самая явная проблема заключается в том, что вы не можете перейти к экрану результатов, так как приложение вылетает. Когда вы вызываете performSegue(withIdentifier: "Results", sender: nil), UIKit использует стандартный требуемый инициализатор для ResultsViewController. Если вы помните, вы сделали так, чтобы этот метод вызывал сбой при вызове. Вам нужно указать вашему Storyboard, какой метод вызывать вместо этого, когда этот segue активируется.

Найдите segue для результатов, который идентифицируется стрелкой между контроллерами вопросов и результатов. Перетащите зажав Control от segue к QuestionsViewController, чтобы создать @IBSegueAction. 34  

Назовите его showResults и установите Arguments в None, затем нажмите Connect. 35  Это вставит метод, аналогичный @IBAction, за исключением того, что он уникален для segue. Метод настроен на возврат необязательного ResultsViewController. Ваша задача в реализации — инициализировать и вернуть его. Используя созданный вами ранее пользовательский инициализатор, верните новый экземпляр ResultsViewController, передав в него предоставленный coder и answersChosen из self.

 

@IBSegueAction func showResults(_ coder: NSCoder) -> 
   ResultsViewController? {
    return ResultsViewController(coder: coder, responses: 
       answersChosen)
}

 

Теперь, когда выполняется segue для результатов, вызывается этот метод, и его возвращаемое значение используется для представления контроллера.

Часть Пятая

Вычисление и отображение результатов

У вас есть рабочий пользовательский интерфейс для различных типов вопросов, и вы записываете ответы игрока. Заключительные шаги включают вычисление результатов, их представление и закрытие экрана результатов, чтобы викторина была готова для нового игрока.

Подсчет частоты ответов

Теперь, когда ResultsViewController имеет ответы игрока, пора подумать о том, как вычислить личность. Какой тип личности был самым распространенным среди выбранных ответов? В этом примере, если игрок дал два ответа, которые соответствовали собаке, и по одному для каждого другого животного, лучший результат будет собака. Если два или более животных имеют равное количество ответов, любой из них будет допустимым результатом.

Как можно подсчитать результаты? Вам нужно пройтись по каждой структуре Answer в свойстве responses и вычислить, какой type был наиболее распространенным в коллекции. Хорошим решением может быть использование модели данных словаря, где ключом является тип ответа, а значением - количество раз, когда игрок выбрал его в качестве ответа, фактически создавая гистограмму. Например, если игрок дал два ответа, которые соответствовали собаке, и по одному для каждого другого животного, словарь будет выглядеть следующим образом:

 

{
  cat : 1,
  dog : 2,
  rabbit : 1,
  turtle : 1
}

 

Чтобы начать, в ResultsViewController объявите метод с именем calculatePersonalityResult, который вы сможете вызвать в viewDidLoad(). В этом методе вы вычисляете частоту каждого ответа, как в приведенном выше коде. Для викторины "Какое вы животное?" вы можете использовать следующий код:

 

override func viewDidLoad() {
    super.viewDidLoad()
    calculatePersonalityResult()
}
 
func calculatePersonalityResult() {
  let frequencyOfAnswers = responses.reduce(into: [:]) {
    (counts, answer) in
        counts[answer.type, default: 0] += 1
    }
}

 

При вычислении результата вам не нужна вся структура Answer. Вас интересует только свойство type каждого Answer. Таким образом, вы можете сократить responses в новый словарь типа [AnimalType: Int], где Int - это частота, с которой игрок выбирал соответствующий AnimalType.

Вот что происходит. reduce(into:) — это метод в массиве, который проходит по каждому элементу, объединяя их в одно значение с использованием кода, который вы предоставляете. Так же как for...in цикл выполняет свой блок кода один раз для каждого элемента в массиве, reduce(into:) выполняет код внутри следующих фигурных скобок один раз для каждого элемента. Но код внутри фигурных скобок отличается от тела цикла. Это замыкание, которое принимает два параметра, которые вы видите как (counts, answer) in. Первый параметр — это элемент, в который вы уменьшаете (редуцируете) значения. Второй параметр — это элемент из коллекции для текущей итерации. (Вы можете найти больше информации о замыканиях в руководстве по языку Swift.)

В этом случае вы уменьшаете (редуцируете) responses в словарь, который изначально пуст — аргумент, переданный для into: — и затем увеличиваете значение в словаре, которое соответствует ключу для каждого ответа. Обратите внимание на параметр default: в подскрипте counts. Это способ защититься от отсутствия ключей в словаре. Если ключ не существует, он создается и устанавливается в значение, указанное для default. Предоставляя значение по умолчанию, вы избегаете дополнительной логики, которая в противном случае была бы необходима для обработки значения nil, которое словарь обычно возвращает, когда ключ не существует.

Следующий код эквивалентен предыдущему, но без использования синтаксиса подстановки по умолчанию:

 

let frequencyOfAnswers = responses.reduce(into: 
   [AnimalType: Int]()) { (counts, answer) in
    if let existingCount = counts[answer.type] {
        counts[answer.type] = existingCount + 1
    } else {
        counts[answer.type] = 1
    }
}

 

Если вы не указываете значение по умолчанию для несуществующего ключа, вам нужно развернуть результат подстановки перед попыткой увеличить его, и вставить новое значение, если оно еще не существует. Обратите внимание, что вам также нужно указать информацию о типе для начального значения параметра into:. Когда 0 было указано как значение по умолчанию для counts, Swift мог сделать вывод, что значения словаря counts должны быть типа Int. (В обоих случаях, Swift может сделать вывод, что ключи будут типа AnimalType, потому что это тот тип, который вы передаете.)

Для получения дополнительной информации, нажмите Option+Click на reduce(into:), чтобы открыть справку. 36  Вы найдете пример использования, который точно соответствует тому, что вы только что сделали — это идеальный метод для этой задачи.

Определение самых частых ответов

Теперь, когда у вас есть словарь, который знает частоту каждого ответа, можно определить, какое значение является наибольшим. Вы можете использовать метод Swift sorted(by:) для словаря, чтобы поместить каждую пару ключ/значение в массив, отсортировав свойства значений в порядке убывания.

 

let frequentAnswersSorted = frequencyOfAnswers.sorted(by:
{ (pair1, pair2) in
    return pair1.value > pair2.value
})
 
let mostCommonAnswer = frequentAnswersSorted.first!.key

 

Как именно работает этот код? Предположим, ваш словарь выглядит следующим образом:

 

{
  cat : 1,
  dog : 2,
  rabbit : 1,
  turtle : 1
}

 

Параметр, который вы передаете в sorted(by:), является замыканием, которое принимает любые две пары ключ-значение. В викторине про животных, pair1 может соответствовать cat : 1, а pair2dog : 2. Внутри тела замыкания вам нужно вернуть логическое значение, чтобы указать, какая из пар больше. В случае return 1 > 2, логическое значение будет false — поэтому метод знает, что pair2 больше, чем pair1.

Когда метод завершится, массив frequentAnswersSorted может выглядеть примерно так:

 

[(dog, 2), (rabbit, 1), (turtle, 1), (cat, 1)]

 

Для пар ключ/значение, которые имеют одинаковое значение, нет способа ранжировать одну пару выше другой — поэтому rabbit, turtle и cat могут быть в разном порядке. Но это не имеет значения, так как вам нужно только первое значение в массиве.

Вы можете упростить замыкание, используя четыре техники. Во-первых, вы можете использовать $0 и $1 в качестве неявных имен параметров без определения собственных параметров, так что вы можете удалить (pair1, pair2) in. Вы также можете ссылаться на элементы пар по номерам, а не по именам, заменив ссылку на .value на 1. Далее, вы можете убрать оператор return для замыканий, которые содержат только одно выражение. Замыкание автоматически возвращает это значение. Наконец, вы можете использовать синтаксис замыканий, расположенных в хвосте, чтобы удалить круглые скобки вокруг вызова sorted(by:) и убрать метку аргумента. Вызов reduce(into:) выше также использовал синтаксис замыканий, расположенных в хвосте. Этот метод имеет два параметра. Вам нужно было передать первый аргумент, используя традиционный синтаксис вызова функции внутри круглых скобок. Затем вы передали последний аргумент — замыкание, расположенное в хвосте — вне круглых скобок.

Используя эти техники и извлекая ключ первого элемента результата, вы можете упростить код до одной строки:

 

let mostCommonAnswer = frequencyOfAnswers.sorted { $0.1 > $1.1 }.first!.key

 

Просмотр результатов

Теперь осталось только обновить текст ваших меток на соответствующие значения внутри метода calculatePersonalityResult. Вам нужно будет добавить некоторые аутлеты в ResultsViewController, чтобы можно было обновить текст каждой метки в коде. Откройте ассистент редактора и перетащите (Control-drag) от каждой метки к пространству в определении класса ResultsViewController. Дайте каждой метке соответствующее имя.

 

@IBOutlet var resultAnswerLabel: UILabel!
@IBOutlet var resultDefinitionLabel: UILabel!

 

Добавьте следующий код в конец метода calculatePersonalityResult, чтобы обновить ваши метки данными, хранящимися в mostCommonAnswer:

 

resultAnswerLabel.text = “You are a \(mostCommonAnswer.rawValue)!”
resultDefinitionLabel.text = mostCommonAnswer.definition

 

Соберите и запустите ваше приложение, и вы наконец-то увидите результаты вашего викторины визуально.

Перезапуск викторины

В большинстве викторин на определение личности игрок проходит все вопросы только один раз. После того как результаты были показаны, игроки не должны иметь возможность вернуться назад и изменить ранее данные ответы, чтобы попытаться достичь другого результата. К сожалению, кнопка "Назад" на экране результатов подразумевает, что они могут это сделать. Чтобы скрыть кнопку "Назад" в навигационной панели, добавьте следующую строку кода в конец метода viewDidLoad() для ResultsViewController:

 

navigationItem.hidesBackButton = true

 

Вместо изменения предыдущих ответов игрок должен иметь возможность закрыть результаты и начать с чистого листа. Нажатие кнопки "Готово" может вернуть к IntroductionViewController, четко указывая, что викторина закончена. Но в данный момент кнопка "Готово" не связана ни с каким действием.

Вам нужно создать метод возврата в первом контроллере представления. Добавьте следующее в определение IntroductionViewController:

 

@IBAction func unwindToQuizIntroduction(segue: UIStoryboardSegue) {
 
}

 

Теперь вы можете связать действие кнопки "Готово" с этим методом возврата в Main.storyboard.

Так как приложению не нужно сохранять или передавать какую-либо информацию при закрытии ResultsViewController, вы можете оставить тело метода пустым.

В сториборде перетащите (Control-drag) от кнопки "Готово" к кнопке "Выход" (Exit) в верхней части контроллера представления. Выберите опцию unwindToQuizIntroductionWithSegue, которая появится в всплывающем меню. 37 

Теперь, когда игрок нажимает кнопку "Готово", разворотный сегвей (unwind segue) закроет контроллеры представления, которые были созданы после отображения IntroductionViewController. Это включает как QuestionViewController, так и ResultsViewController.

Завершение работы

Поздравляем с созданием пользовательского приложения викторины для iOS!

Этот проект был насыщенным, и в интерфейсе происходило много всего. Если у вас возникли трудности с созданием стековых представлений, добавлением ограничений, созданием выходов (outlets) или изменением атрибутов представлений и элементов управления, уделите время практике работы с Interface Builder. Это один из ваших самых ценных инструментов для создания приложений, который сократит количество кода, который пришлось бы писать, если бы вы создавали интерфейс программно.

Если у вас возникли проблемы с кодом на Swift, помните, что вы всегда можете обратиться к документации Xcode для получения дополнительной информации о методах и свойствах, или обратиться к предыдущему блоку, который охватывал синтаксис и типы данных Swift.

Дополнительные цели

Хотите расширить вашу викторину по личностям еще больше? Есть ли шаги, которые вы хотели бы улучшить? Вот несколько идей, которые сделают вопросы более динамичными и добавят повторяемость вашему приложению:

  • Позвольте игроку выбирать между несколькими викторинами по личностям с экрана введения.
  • Рандомизируйте порядок представления вопросов, а также порядок ответов.
  • Разрешите вопросам с одним и несколькими ответами иметь динамическое количество ответов, а не всегда четыре. Подсказка: Вместо создания элементов управления в Interface Builder, вам нужно будет программно добавлять/удалять метки и элементы управления из стековых представлений.

 

 

 


Отрывок из книги
Develop in Swift Fundamentals
Apple Education
https://books.apple.com/ru/book/develop-in-swift-fundamentals/id1581182804

Information

Apple, the Apple logo, Apple Books, Apple TV, Apple Watch, Cocoa, Cocoa Touch, Finder, Handoff, HealthKit, iPad, iPad Pro, iPhone, iPod touch, Keynote, Mac, macOS, Numbers, Objective-C, Pages, Photo Booth, Safari, Siri, Spotlight, Swift, tvOS, watchOS, and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries. App Store and iBooks Store are service marks of Apple Inc., registered in the U.S. and other countries. ​
The Bluetooth® word mark and logos are registered trademarks owned by Bluetooth SIG, Inc. and any use of such marks by Apple is under license. ​
IOS is a trademark or registered trademark of Cisco in the U.S. and other countries and is used under license. ​
Other product and company names mentioned herein may be trademarks of their respective companies.