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

Источник: https://www.youtube.com/watch?v=tvVj6MSBhBA
Канал: Stanford Online
Опубликовано: 25.11.2025

---

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

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

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

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

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

* Создал новый файл из стандартного шаблона SwiftUI View под названием `PegView.swift`.
* Реализовал явный интерфейс входящих данных (`Data In`), добавив свойство `let peg: Peg`.
* Настроил блок макетирования `#Preview`, передав туда дефолтное значение для визуализации.

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

## 🎨 Архитектурный подход к константам и кастомная палитра
[[JUMP: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
[[JUMP:11:10]]

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

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

Для изменения выбранной фишки в модели лектор написал функцию:
```swift
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
[[JUMP:43:04]]

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

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



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

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

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

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

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

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

* **Choices (`Data In`):** Простой массив фишек `let choices: [Peg]`, которые нужно отобразить на экране.
* **onChoose (`Data Out`):** Замыкание обратного вызова (callback closure) вида `let onChoose: (Peg) -> Void`, сигнализирующее о том, что пользователь выбрал конкретный цвет.

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

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

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

## 👁️ Скрытие мастер-кода, опциональные цепочки и борьба с кэшем Xcode
[[JUMP: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 и асинхронность
[[JUMP:1:09:35]]

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

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

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