Стэнфордский курс CS193p: проектирование независимой модели и управления состоянием в SwiftUI

Stanford Online 10,1 тыс. 1 ч 11 мин 8 мин 25.11.2025
Главное

Пол Хэгарти, лектор Стэнфордского университета, в рамках четвертой лекции курса CS193p (весна 2025 года) подробно разбирает разделение приложения CodeBreaker на независимую модель (Model) и интерфейс (UI). В процессе живого кодинга демонстрируются ключевые концепции разработки под iOS: от управления состоянием с помощью макроса @State до работы со связанными значениями перечислений (Associated Values) и оптимизации рендеринга. В результате занятия игра получает полностью рабочий игровой процесс, управляемый строгой бизнес-логикой.

🛠️ Создание независимой модели: архитектура и шаблоны файлов Xcode 0:34

Проектирование чистого приложения начинается с отделения бизнес-логики от визуального слоя. В начале лекции UI представляет собой обычный прототип с жестко зашитыми тестовыми значениями. Чтобы превратить интерфейс в полноценное отображение логики, создается модель на основе структур данных. В долгосрочной перспективе, как отмечает Пол Хэгарти, модель CodeBreaker эволюционирует в полноценную базу данных SQL, однако на старте достаточно использовать легковесные структуры Swift.

При создании новых компонентов в Xcode Пол Хэгарти настоятельно рекомендует использовать встроенные шаблоны файлов:

Важным элементом структуры проекта становится организация файлов по папкам («Groups»). По мнению лектора, файлы не должны хаотично лежать на самом верхнем уровне навигатора. Их следует структурировать по субдиректориям для удобства поддержки и масштабирования приложения.

🎨 Типы данных модели и компромисс с импортом UI-компонентов 3:05

Внутри файла CodeBreaker.swift описывается базовая структура игры, содержащая ключевые переменные хранения: секретный код (masterCode), текущая догадка (guess), массив прошлых попыток (attempts) и набор доступных цветов для фишек (pegChoices).

Чтобы сделать код семантически понятным, Пол Хэгарти использует механизм typealias, переименовывая системный тип цвета Color в игровой термин Peg. Это служит отличным способом самодокументирования кода, подсказывая другим разработчикам истинное назначение сущности.

Тем не менее, интеграция Color напрямую в модель порождает серьезную архитектурную проблему, на которую лектор обращает особое внимание:

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

🔄 Рефакторинг и интеграция модели в пользовательский интерфейс 13:06

Для связывания модели с интерфейсом Пол Хэгарти внедряет экземпляр игры непосредственно в структуру представления. На этом этапе стандартное имя файла ContentView, сгенерированное Xcode по умолчанию, переименовывается в CodeBreakerView, чтобы точнее отражать суть экрана.

Процесс тотального переименования сущностей по всему проекту выполняется с помощью встроенного инструмента рефакторинга:

  1. Разработчик выбирает имя типа, вызывает контекстное меню правой кнопкой мыши и переходит в раздел «Refactor -> Rename».
  2. Xcode автоматически находит все семантические упоминания во всех файлах проекта, включая конфигурацию главного приложения и превью.
  3. Исключением могут стать текстовые комментарии в шапке файлов — среда разработки подсвечивает их отдельно, позволяя программисту вручную подтвердить необходимость изменения.

После успешного перенаправления связей интерфейс переключается на динамическое чтение данных из модели. Вместо хардкодных прототипов строки элементов начинают считывать информацию напрямую через свойства game.masterCode.pegs и game.guess.pegs.

⚡ Обработка жестов и изменяемость данных: ключевое отличие структур от классов 24:33

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

Здесь разработчики сталкиваются с фундаментальным поведением структур Swift — их иммутабельностью (неизменяемостью) по умолчанию. Попытка изменить элемент внутри массива фишек приводит к ошибке компиляции. Для решения этой проблемы функция модели должна быть явно помечена ключевым словом mutating. Это указывает компилятору на необходимость применения механизма Copy-on-Write (копирование при записи):

Однако вызов мутирующей функции модели из тела интерфейса (var body) вновь блокируется компилятором. Пол Хэгарти категорически предостерегает от попыток создавать mutating-функции внутри самих View. Поскольку свойство body является вычисляемым и доступным только для чтения, компоненты интерфейса SwiftUI по определению иммутабельны и находятся внутри системных констант.

🧠 Управление состоянием через @State и автоматический подсчет ссылок (ARC) 36:52

Единственным легитимным способом разрешить мутацию данных внутри представления является использование свойства-обертки (макроса) @State. Под капотом этой конструкции скрывается изящное техническое решение. Макрос создает скрытую переменную с префиксом подчеркивания (_game), внутри которой находится указатель на экземпляр класса, размещенный в куче (heap).

Когда код обращается к переменной game, происходит скрытая индирекция (перенаправление) через этот указатель наружу, в область динамической памяти. Использование @State предоставляет разработчикам важные преимущества:

В контексте управления памятью Пол Хэгарти детально разъясняет разницу между структурами и классами. В языке 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:

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

📐 Точечная настройка интерфейса: оверлеи, масштабирование шрифтов и невидимые касания 1:01:33

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

Для обеспечения адаптивности текста кнопки применяется связка двух модификаторов:

При очистке игрового поля лектор сталкивается с неочевидной особенностью обработки экранных жестов. Когда фишка имеет прозрачный цвет .clear, любые касания пользователя проходят сквозь неё, делая элемент некликабельным. Проблема оперативно устраняется добавлением модификатора .contentShape(Rectangle()). Данная команда принуждает операционную систему iOS интерпретировать невидимую область как плотный осязаемый прямоугольник, корректно перехватывающий любые мультитач-события.

Занятие завершается созданием полноценного инициализатора init(), который автоматически генерирует случайную комбинацию с помощью системного метода randomElement() и гибко принимает кастомные палитры цветов от вызывающей стороны, используя ключевое слово self. для разграничения одноименных параметров в области видимости.

💬 Цитаты

«Мы никогда не используем мутирующие функции внутри View. Все View находятся внутри большого константного 'let' где-то в системе.»

Пол Хэгарти 35:54

«В Swift нет сборщика мусора. Вместо этого используется автоматический подсчет ссылок, который применяется только к классам.»

Пол Хэгарти 44:19
👥 Спикер
📖 Термины
typealias
Создание псевдонима для существующего типа данных, улучшающее читаемость кода.
copy-on-write
Механизм оптимизации памяти, копирующий данные структуры только в момент их изменения.
Associated Values
Связанные данные в перечислениях Swift, позволяющие хранить дополнительную информацию для конкретных кейсов.
ViewBuilder
Специальный макрос (Result Builder) в SwiftUI, позволяющий конструировать интерфейс из цепочки вложенных компонентов.
📊 Цифры
🗓 Хронология
  1. 2025 Проведение курса CS193p по разработке приложений под iOS в Стэнфордском университете.
⚖️ Другая сторона
Технологии и IT SwiftUI iOS Пол Хэгарти CS193p Стэнфорд