Пол Хэгарти, лектор Стэнфордского университета, в рамках четвертой лекции курса CS193p (весна 2025 года) подробно разбирает разделение приложения CodeBreaker на независимую модель (Model) и интерфейс (UI). В процессе живого кодинга демонстрируются ключевые концепции разработки под iOS: от управления состоянием с помощью макроса @State до работы со связанными значениями перечислений (Associated Values) и оптимизации рендеринга. В результате занятия игра получает полностью рабочий игровой процесс, управляемый строгой бизнес-логикой.
🛠️ Создание независимой модели: архитектура и шаблоны файлов Xcode 0:34
Проектирование чистого приложения начинается с отделения бизнес-логики от визуального слоя. В начале лекции UI представляет собой обычный прототип с жестко зашитыми тестовыми значениями. Чтобы превратить интерфейс в полноценное отображение логики, создается модель на основе структур данных. В долгосрочной перспективе, как отмечает Пол Хэгарти, модель CodeBreaker эволюционирует в полноценную базу данных SQL, однако на старте достаточно использовать легковесные структуры Swift.
При создании новых компонентов в Xcode Пол Хэгарти настоятельно рекомендует использовать встроенные шаблоны файлов:
- Выбор опции «File from Template» позволяет среде разработки автоматически вносить файл в список сборки и корректно выстраивать дерево зависимостей проекта.
- Использование пустого файла («Empty File») не рекомендуется, так как лишает разработчика автоматической интеграции.
- Для компонентов модели выбирается стандартный шаблон «Swift File», поскольку модель принципиально изолирована от UI-фреймворков и не является визуальным представлением.
Важным элементом структуры проекта становится организация файлов по папкам («Groups»). По мнению лектора, файлы не должны хаотично лежать на самом верхнем уровне навигатора. Их следует структурировать по субдиректориям для удобства поддержки и масштабирования приложения.
🎨 Типы данных модели и компромисс с импортом UI-компонентов 3:05
Внутри файла CodeBreaker.swift описывается базовая структура игры, содержащая ключевые переменные хранения: секретный код (masterCode), текущая догадка (guess), массив прошлых попыток (attempts) и набор доступных цветов для фишек (pegChoices).
Чтобы сделать код семантически понятным, Пол Хэгарти использует механизм typealias, переименовывая системный тип цвета Color в игровой термин Peg. Это служит отличным способом самодокументирования кода, подсказывая другим разработчикам истинное назначение сущности.
Тем не менее, интеграция Color напрямую в модель порождает серьезную архитектурную проблему, на которую лектор обращает особое внимание:
- Тип
Colorв экосистеме Apple не является абстрактным цветом. Он неразрывно связан с визуальным интерфейсом. - Система автоматически корректирует оттенки (например, зеленый цвет) при переключении между светлой и темной темами оформления, чтобы человеческий мозг воспринимал их одинаково комфортно.
- Использование
Colorв модели нарушает золотое правило независимости бизнес-логики от UI. Из-за этого приходится заменять чистый импорт библиотекиFoundationна модульSwiftUICore.
Пол Хэгарти сознательно идет на этот шаг ради упрощения лекционного материала, однако дает студентам задачу исправить эту ошибку в домашней работе. В рамках учебного задания тип Peg превратится в обычную строку String, хранящую текстовое название цвета или эмодзи, а за конвертацию строк в визуальные стили будет отвечать исключительно слой представления.
🔄 Рефакторинг и интеграция модели в пользовательский интерфейс 13:06
Для связывания модели с интерфейсом Пол Хэгарти внедряет экземпляр игры непосредственно в структуру представления. На этом этапе стандартное имя файла ContentView, сгенерированное Xcode по умолчанию, переименовывается в CodeBreakerView, чтобы точнее отражать суть экрана.
Процесс тотального переименования сущностей по всему проекту выполняется с помощью встроенного инструмента рефакторинга:
- Разработчик выбирает имя типа, вызывает контекстное меню правой кнопкой мыши и переходит в раздел «Refactor -> Rename».
- Xcode автоматически находит все семантические упоминания во всех файлах проекта, включая конфигурацию главного приложения и превью.
- Исключением могут стать текстовые комментарии в шапке файлов — среда разработки подсвечивает их отдельно, позволяя программисту вручную подтвердить необходимость изменения.
После успешного перенаправления связей интерфейс переключается на динамическое чтение данных из модели. Вместо хардкодных прототипов строки элементов начинают считывать информацию напрямую через свойства game.masterCode.pegs и game.guess.pegs.
⚡ Обработка жестов и изменяемость данных: ключевое отличие структур от классов 24:33
Для реализации интерактивности Пол Хэгарти добавляет обработку касаний с помощью модификатора .onTapGesture на уровне графических прямоугольников фишек. Задача интерфейса ограничивается лишь улавливанием физического действия пользователя, после чего управление передается модели для циклического перебора доступных цветов.
Здесь разработчики сталкиваются с фундаментальным поведением структур Swift — их иммутабельностью (неизменяемостью) по умолчанию. Попытка изменить элемент внутри массива фишек приводит к ошибке компиляции. Для решения этой проблемы функция модели должна быть явно помечена ключевым словом mutating. Это указывает компилятору на необходимость применения механизма Copy-on-Write (копирование при записи):
- Если структура передается в коде, Swift не копирует все её биты в памяти вхолостую, экономя системные ресурсы.
- Реальное копирование и выделение новой памяти происходят только тогда, когда приложение пытается перезаписать измененное значение в переменную.
Однако вызов мутирующей функции модели из тела интерфейса (var body) вновь блокируется компилятором. Пол Хэгарти категорически предостерегает от попыток создавать mutating-функции внутри самих View. Поскольку свойство body является вычисляемым и доступным только для чтения, компоненты интерфейса SwiftUI по определению иммутабельны и находятся внутри системных констант.
🧠 Управление состоянием через @State и автоматический подсчет ссылок (ARC) 36:52
Единственным легитимным способом разрешить мутацию данных внутри представления является использование свойства-обертки (макроса) @State. Под капотом этой конструкции скрывается изящное техническое решение. Макрос создает скрытую переменную с префиксом подчеркивания (_game), внутри которой находится указатель на экземпляр класса, размещенный в куче (heap).
Когда код обращается к переменной game, происходит скрытая индирекция (перенаправление) через этот указатель наружу, в область динамической памяти. Использование @State предоставляет разработчикам важные преимущества:
- Поиск по ключевому слову
@Stateв кодовой базе позволяет мгновенно обнаружить все легитимные источники истины (Sources of Truth) внутри интерфейса. - Изменяемость изолируется рамками конкретной переменной, не затрагивая остальные константные свойства View.
В контексте управления памятью Пол Хэгарти детально разъясняет разницу между структурами и классами. В языке Swift полностью отсутствует классический сборщик мусора (Garbage Collection) с его тяжелыми алгоритмами маркировки и очистки. Instead применяется механизм автоматического подсчета ссылок (Automatic Reference Counting, ARC), который управляет исключительно классами. Структуры же, будучи значимыми типами, не требуют подсчета ссылок — они создаются прямо по месту вызова и мгновенно уничтожаются в памяти, как только выходят из своей области видимости.
📜 Списки попыток, ScrollView и реактивная анимация изменений 39:14
Для отображения истории ходов используется контейнер ForEach, итерирующийся по уникальным индексам массива попыток. При нажатии на созданную кнопку «Guess» модель выполняет копирование текущей догадки, меняет её внутренний тип на .attempt и добавляет в историю. Благодаря реактивной природе SwiftUI, интерфейс мгновенно реагирует на обновление модели и автоматически перерисовывает только изменившиеся элементы экрана.
В ходе тестирования Пол Хэгарти критикует стандартное поведение получившегося интерфейса, выделяя три ключевые проблемы:
- Кнопка отправки постоянно смещается вниз при добавлении новых строк, «выскальзывая» из-под пальца пользователя, что мешает комфортной игре.
- Новые попытки добавляются в нижнюю часть экрана, заставляя игрока постоянно переводить взгляд вверх и вниз для анализа ситуации.
- При большом количестве ходов элементы неизбежно выходят за физические границы дисплея, перекрывая секретный код.
Проблемы решаются эргономической оптимизацией. Лектор инвертирует порядок вывода элементов с помощью метода .reversed(), закрепляя свежие попытки вверху рядом с зоной ввода. Затем весь список оборачивается в компонент ScrollView, который занимает всё доступное пространство экрана и фиксирует кнопку отправки в самом низу. Наконец, для устранения резких визуальных скачков код оборачивается в блок withAnimation. Это заставляет SwiftUI плавно сдвигать старые строки вниз и проявлять новые элементы через изменение прозрачности (opacity).
🏷️ Связанные данные в перечислениях (Associated Values) и протокол Equatable 50:22
Для отображения черно-белых маркеров точных и частичных совпадений модель дополняется функцией match(against:), возвращающей массив результатов. Однако расчет маркеров имеет смысл только для прошлых ходов (attempt), но абсолютно бесполезен для секретного кода или текущей редактируемой догадки.
Чтобы изящно связать данные с конкретным состоянием, Пол Хэгарти использует связанные значения перечислений (Associated Values), модифицируя кейс attempt кодом case attempt([Match]). Это позволяет хранить массив маркеров исключительно там, где это обусловлено логикой игры. Для удобного извлечения данных создается вычисляемое свойство, использующее конструкцию switch.
Внедрение связанных данных ломает встроенную проверку на равенство (==), так как компилятор перестает понимать, как сравнивать ассоциированные массивы. Лектор решает эту проблему подпиской перечисления Kind на протокол Equatable:
- Протокол
Equatableтребует наличия статической функции==, принимающей левую и правой стороны для сравнения. - Если перечисление не имеет связанных данных, компилятор синтезирует эту функцию автоматически за кулисами.
- При добавлении ассоциированных значений автоматический синтез сохраняется только в том случае, если сами передаваемые данные также удовлетворяют условиям
Equatable.
Поскольку массив результатов состоит из базовых элементов Match, не имеющих скрытых параметров, Swift успешно генерирует код сравнения самостоятельно.
📐 Точечная настройка интерфейса: оверлеи, масштабирование шрифтов и невидимые касания 1:01:33
В финальной части занятия Пол Хэгарти демонстрирует несколько продвинутых техник верстки в SwiftUI. Кнопка отправки переносится из нижней панели непосредственно в область маркеров текущей догадки с помощью модификатора .overlay. В отличие от ZStack, который просто накладывает слои друг на друга, .overlay принудительно масштабирует дочерний элемент, вписывая его строго в физические границы родительского компонента.
Для обеспечения адаптивности текста кнопки применяется связка двух модификаторов:
- Системный шрифт
.systemнастраивается на заведомо огромный размер в 80 пунктов. - Модификатор
.minimumScaleFactor(0.1)разрешает тексту динамически сжиматься вплоть до 10% от исходного размера (до 8 пунктов), гарантируя идеальное заполнение выделенного пространства без обрезки букв.
При очистке игрового поля лектор сталкивается с неочевидной особенностью обработки экранных жестов. Когда фишка имеет прозрачный цвет .clear, любые касания пользователя проходят сквозь неё, делая элемент некликабельным. Проблема оперативно устраняется добавлением модификатора .contentShape(Rectangle()). Данная команда принуждает операционную систему iOS интерпретировать невидимую область как плотный осязаемый прямоугольник, корректно перехватывающий любые мультитач-события.
Занятие завершается созданием полноценного инициализатора init(), который автоматически генерирует случайную комбинацию с помощью системного метода randomElement() и гибко принимает кастомные палитры цветов от вызывающей стороны, используя ключевое слово self. для разграничения одноименных параметров в области видимости.