Лекция Стэнфорда CS193p: проектирование потоков данных и декомпозиция в SwiftUI

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

В шестой лекции знаменитого курса Стэнфордского университета CS193p (весна 2025 года) основной фокус полностью смещен на практическую демонстрацию управления потоками данных (Data Flow) в SwiftUI. Лектор детально разбирает процесс рефакторинга кода, декомпозиции представлений и создания чистой архитектуры приложения CodeBreaker. Главной задачей занятия становится переход от хаотичного связывания состояний к осознанному проектированию интерфейсов данных с использованием единых источников истины.

🛠️ Ошибки рефакторинга и великое переселение структур 0:05

В программировании даже случайные ошибки могут натолкнуть на правильные архитектурные решения. Проводя работу над кодом предыдущего занятия, лектор случайно применил команду Extract Selection to File вместо обычного переименования (Rename) для сущности .missingPeg. Этот шаг, изначально показавшийся ошибочным, в итоге продемонстрировал удобство автоматического разделения кода в Xcode. Структура Code обрела собственный одноименный файл Code.swift. По мнению преподавателя, это было необходимо, поскольку Code представляет собой слишком важный и комплексный компонент логики, чтобы делить пространство с другими структурами. Этот инструмент рефакторинга значительно упрощает жизнь разработчика, избавляя от необходимости вручную создавать пустые файлы Swift, копировать и вставлять код.

Следующим шагом стало выделение визуального элемента фишки (peg) в отдельное специализированное представление — так называемое «вертолетное представление» (helicopter View). Весь блок кода, отвечающий за отрисовку элемента RoundedRectangle со всеми его модификаторами и наложениями (.overlay), был извлечен из основного экрана CodeBreakerView и заменен аккуратной структурой PegView.

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

В процессе работы лектор уделил особое внимание дисциплине оформления кода с помощью комментариев специального вида. Конструкция // MARK: - выполняет в Xcode сразу две важнейшие функции: она визуально проводит тонкую разделительную линию прямо в редакторе текста и генерирует наглядные флаги-метки в панели быстрой навигации по файлу. Лектор рекомендует внедрять такие метки для четкого разделения блоков входящих данных (Data In), состояний (@State) и основного тела интерфейса (Body). В рамках этой же дисциплины свойство var в структуре MatchMarkers было заменено на let. Как считает лектор, если свойство инициализируется извне один раз и не имеет дефолтного значения, использование let гарантирует иммутабельность и защищает от случайных перезаписей.

🎨 Архитектурный подход к константам и кастомная палитра 7:01

Одной из самых раздражающих особенностей работы с интерфейсом Xcode является автоматическое исчезновение панели предварительного просмотра (#Preview) при переключении с файла представления на файлы логики или моделей. Для решения этой проблемы лектор продемонстрировал функцию фиксации холста (кнопка с изображением булавки — Pin). Будучи закрепленным, превью остается на экране, даже если разработчик уходит редактировать модель данных, позволяя мгновенно видеть изменения интерфейса в реальном времени.

Внедрение новой функциональности началось с добавления механизма сброса текущей догадки (guess.reset()) после отправки пользователем своего варианта. Преподаватель подчеркнул, что в декларативном подходе SwiftUI вся магия заключается в том, что интерфейс является лишь визуальным отражением модели. Достаточно реализовать мутирующую функцию сброса внутри структуры Code (перезаписывающую массив фишек дефолтными значениями Code.missingPeg), как пользовательский интерфейс автоматически обновится и даже анимирует это действие благодаря обертке withAnimation.

Важной вехой лекции стала борьба с «магическими числами» (magic numbers) — жестко захардкоженными значениями размеров, радиусов и прозрачности, разбросанными по коду. С точки зрения лектора, такие «синие цифры» чрезвычайно опасны при масштабировании приложения под другие платформы (например, iPad или Apple Watch), поскольку они ломают системный адаптивный макет.

Для борьбы с ними был применен паттерн создания вложенных структур констант. Внутри CodeBreakerView лектор объявил типы Selection и GuessButton, содержащие исключительно свойства static let. Это дало коду три важнейших преимущества:

  1. Полная изоляция констант в едином пространстве имен (например, CodeBreakerView.Selection.border).
  2. Возможность использования вывода типов Swift внутри области видимости этой структуры.
  3. Повышение уровня самодокументируемости кода: имя константы само объясняет её предназначение лучше любых комментариев.

Параллельно была решена проблема создания светлых оттенков серого цвета. Стандартная системная палитра Color в SwiftUI не имеет встроенного свойства lightGray (оно осталось в старом фреймворке UIColor). Лектор написал элегантное расширение (extension) для Color, добавив статическую функцию gray(brightness:). Внутри неё используется инициализатор, принимающий параметры цветового пространства HSB (оттенок, насыщенность, яркость). Это позволило гибко настраивать яркость подложки выбранного элемента для корректного отображения как в светлой, так и в темной темах оформления, избегая использования полупрозрачности (.opacity), которая делала бы объект зависимым от фонового изображения.

⌨️ Проектирование PegChooser и безопасный код с guard 11:10

Постоянное циклическое перекликивание фишек для выбора цвета утомляет пользователя. Лектор предложил альтернативное решение — создание своеобразной виртуальной клавиатуры выбора цветов (pegChooser) в нижней части экрана. Для этого был задействован цикл ForEach, итерирующийся по доступным цветам игры (game.pegChoices). Поскольку все доступные цвета уникальны по своей природе, лектор отказался от использования обращения по индексам (.indices), применив идентификацию по самому элементу (id: \.self). Лектор попутно заметил, что на этапе проектирования модели коллекцию pegChoices правильнее было бы сделать типом Set (множество), а не Array, так как элементы множества обязаны быть уникальными и реализовывать протокол Hashable.

Вместо привычных текстовых кнопок для клавиатуры был использован продвинутый инициализатор Button, позволяющий передавать в качестве графического ярлыка (label) абсолютно любое кастомное представление. Лектор повторно использовал компонент PegView, передавая туда текущий цвет фишки из цикла. При создании такой кнопки используется синтаксис замыкания (trailing closure syntax), где первое замыкание отвечает за выполняемое действие (action), а второе, с явным указанием имени аргумента label:, формирует внешний вид кнопки.

Для изменения выбранной фишки в модели лектор написал функцию:

mutating func setGuessPeg(peg: Peg, at index: Int) {
    guard guess.pegs.indices.contains(index) else { return }
    guess.pegs[index] = peg
}

Здесь был продемонстрирован защитный оператор guard. В отличие от классического if, guard буквально заявляет читателю кода: «Я защищаю эту функцию от критической ошибки». Он проверяет, входит ли переданный индекс в безопасные границы массива, предотвращая фатальный краш приложения из-за выхода за пределы диапазона (index out of bounds).

Само состояние текущего выбора было оформлено как @State private var selection = 0, указывая на то, что это локальные данные, которыми безраздельно владеет само представление CodeBreakerView.

🔄 Кошмар дублирования состояний и синхронизация через @Binding 43:04

Архитектурные проблемы начались тогда, когда лектор решил произвести повторную декомпозицию кода и вынес логику отображения комбинации фишек в отдельный компонент CodeView. Вместе с версткой внутрь нового файла перекочевало и локальное состояние выбора фишки, также помеченное как @State private var selection.

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

Причиной этого поведения стал так называемый «кошмар потоков данных» — появление двух изолированных источников истины (Sources of Truth) для одной и той же сущности. Компоненты работали независимо, не делясь информацией друг с другом. Решением этой фундаментальной проблемы стало использование обертки свойства @Binding. Лектор убрал ключевое слово private и начальное значение у свойства selection в подчиненном представлении CodeView, превратив его в @Binding var selection: Int.

Поясняя внутреннее устройство SwiftUI, преподаватель отметил, что @Binding под капотом не является классическим указателем (pointer) в стиле старых языков программирования. На самом деле, передаваемый Binding-объект содержит встроенные вычисляемые свойства (геттер и сеттер), которые обращаются напрямую к родительскому @State. Для передачи такого моста между представлениями в родительском компоненте перед именем переменной ставится специальный символ знака доллара: $selection. Это возвращает коду архитектурную чистоту, связывая подчиненный компонент с единственным законным источником истины.

📐 Семантическое проектирование: Перестаньте передавать всё состояние целиком 51:44

Закрепляя тему общего доступа к данным, лектор вынес нижнюю клавиатуру в компонент PegChooser. Первой очевидной (но в корне неверной) реализацией стало объявление внутри PegChooser двух жестких привязок: @Binding var game: CodeBreaker и @Binding var selection: Int. Код компилировался и работал без ошибок, однако лектор назвал такой подход «абсолютно ужасным».

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

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

// MARK: Data In
let choices: [Peg]

// MARK: Data Out Function
let onChoose: ((Peg) -> Void)?

Лектор пошел еще дальше и сделал замыкание onChoose опциональным типом. В Swift функции являются полноправными объектами, поэтому они могут принимать значение nil. При вызове такого замыкания используется знак вопроса: onChoose?(peg). Если вызывающая сторона не передала функцию обработки, эта строчка кода просто ничего не сделает, что позволяет безопасно использовать PegChooser в режиме обычного пассивного отображения элементов палитры. На стороне родительского представления вызов трансформировался в лаконичную и понятную конструкцию с передачей массива цветов и обработкой выбранного элемента внутри элегантного блока trailing closure.

👁️ Скрытие мастер-кода, опциональные цепочки и борьба с кэшем Xcode 1:00:01

Финальным штрихом игрового процесса стало скрытие загаданной комбинации (mastercode) от глаз игрока до момента окончания партии. Лектор реализовал это наложением поверх фишек непрозрачной серой заглушки с помощью модификатора .overlay. Показ заглушки завязали на вычисляемое свойство code.isHidden.

Для реализации этого в модели перечисление видов кодов (Kind) было модернизировано: кейсу master добавили именованные ассоциированные данные (case master(isHidden: Bool)). В вычисляемом свойстве isHidden лектор применил конструкцию switch, возвращающую внутренний флаг для мастер-кода и жесткое значение false для обычных догадок и попыток, поскольку их скрывать не имеет смысла.

Определять окончание игры (isOver) лектор предложил также через вычисляемое свойство, сравнивающее фишки последней попытки с загаданным кодом: attempts.last?.pegs == mastercode.pegs. Свойство .last у коллекций в Swift возвращает опциональное значение, так как массив может быть пуст. Здесь лектор продемонстрировал механизм «опциональной цепочки» (Optional chaining) с помощью знака вопроса. Если массив attempts пуст, выражение attempts.last вернет nil, и вся дальнейшая цепочка вычислений мгновенно прервется, вернув общий результат nil. Swift умеет сравнивать nil с обычными объектами, поэтому проверка равенства вернет false, и игра продолжится, не приводя к сбоям программы.

В процессе компиляции обновленного кода Xcode неожиданно выдал критическую системную ошибку доступа к памяти EXC_BAD_ACCESS. Лектор успокоил студентов, объяснив, что подобный сбой — частый признак того, что Xcode запутался в кэшировании зависимостей после глубокого изменения связанных структур данных. Способ лечения этой проблемы прост, не требует затрат и рекомендуется к применению в любой непонятной ситуации: нужно полностью очистить папку сборки через меню Product -> Clean Build Folder. После тотальной очистки проект успешно собрался и продемонстрировал корректное открытие загаданных цветов в момент финального выигрышного хода.

🌍 Подготовка к новой игре: @Environment, @Entry и асинхронность 1:09:35

В заключительной части лекции преподаватель анонсировал домашнее задание Assignment 3 — создание новой игры, похожей на CodeBreaker, но завязанной на угадывание реальных английских слов (аналог популярной игры Wordle). Для обеспечения студентов базой слов лектор интегрировал в проект вспомогательный класс Words.swift, загружающий словарь объемом в 2118 слов прямо с серверов Стэнфорда через интернет. Класс предоставляет методы проверки существования слова contains(word:) и выдачи случайного секретного слова заданной длины.

Доступ к этому словарю внутри представлений SwiftUI реализуется через механизм переменных окружения (@Environment). Лектор показал современный синтаксис Swift, использующий макрос @Entry для быстрого и простого добавления кастомных значений в структуру EnvironmentValues: @Environment(\.words) var words.

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

.onChange(of: words.count) { oldCount, newCount in
    // Логика инициализации игры после загрузки словаря
}

Модификатор .onChange(of:) запускает блок кода каждый раз, когда меняется отслеживаемая переменная. Вспомогательный код в домашнем задании будет изначально выставлять временное секретное слово "await" («ожидание»), а как только свойство words.count сообщит о загрузке всех двух тысяч слов, приложение автоматически переключится на генерацию полноценного случайного секретного слова, не ломая текущую сессию пользователя. Лектор отметил, что архитектура файла Words.swift включает продвинутые темы асинхронного программирования и обработки сетевых ошибок, к подробному изучению которых курс вернется чуть позже.

💬 Цитаты

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

Лектор Стэнфорда 02:08

«Имя константы само объясняет её предназначение лучше любых комментариев.»

Лектор Стэнфорда 38:34

«В отличие от классического if, guard буквально заявляет читателю кода: «Я защищаю эту функцию от критической ошибки».»

Лектор Стэнфорда 19:37
👥 Спикер
🔗 Упомянутые сайты и проекты
📖 Термины
Data Flow
Архитектурный подход в SwiftUI, управляющий передачей, синхронизацией и изменением состояний данных между компонентами интерфейса.
@Binding
Обертка свойства в SwiftUI, создающая двухстороннюю связь между подчиненным представлением и источником истины, принадлежащим родительскому компоненту.
Helicopter View
Ироничный термин лектора, обозначающий изолированное, независимое подпредставление, решающее одну узкую задачу отображения.
Optional Chain
Опциональная цепочка в Swift — безопасный вызов свойств и методов через знак вопроса, прерывающий вычисление при обнаружении nil.
@Entry
Современный макрос в Swift для лаконичного добавления новых кастомных свойств в глобальное окружение EnvironmentValues.
📊 Цифры
⚖️ Другая сторона
Технологии и IT SwiftUI CS193p Stanford University iOS Data Flow