Шестнадцатая лекция курса Stanford CS193p (выпуск 2025 года) посвящена продвинутым техникам разработки на SwiftUI, которые критически важны для завершения финальных проектов. Профессор Стэнфорда подробно разбирает создание собственных геометрических фигур, обработку сложных мультитач-жестов и работу с файловой системой устройства для обеспечения персистентности данных за пределами SwiftData.
💎 Создание кастомных фигур и протокол Shape 1:05
В SwiftUI все встроенные фигуры, такие как Rectangle или Circle, соответствуют протоколу Shape. Этот протокол наследуется от View, что позволяет использовать фигуры везде, где допустимы компоненты интерфейса . Чтобы создать собственную фигуру, необходимо реализовать обязательную функцию path(in rect: CGRect), которая возвращает объект Path.
Основные характеристики работы с Path:
- 2D-рисование: Это структура, предоставляющая методы для перемещения «пера» (
move(to:)), рисования линий (addLine(to:)) и кривых Безье . - Контекст рисования: Параметр
rectтипаCGRectуказывает границы, в которых должна быть отрисована фигура. Это позволяет фигуре адаптивно заполнять предоставленное ей пространство. - Заполнение и обводка: По умолчанию фигуры заполнены (
fill), но их можно отрисовать контуром с помощью метода.stroke().
В качестве примера в лекции создается фигура Diamond (ромб). Для её реализации вычисляются ключевые точки прямоугольника: midX, minY, minX, midY, maxY и maxX . Завершение контура выполняется методом path.closeSubpath(), который автоматически соединяет последнюю точку с начальной . Профессор отмечает, что создание кастомных фигур полезно не только для графики, но и для настройки области нажатия через модификатор .contentShape() .
📐 Мастерство макета с GeometryReader 8:52
GeometryReader — это специальный тип View, который позволяет дочерним элементам узнать размер и положение контейнера, в котором они находятся. Профессор подчеркивает, что этот инструмент намеренно не вводился в курс раньше из-за его «агрессивного» поведения: GeometryReader всегда стремится занять всё доступное пространство, подобно распорке (spacer), что может нарушить тонкую настройку макета .
Ключевые сценарии использования GeometryReader:
- Локальные координаты: Получение кадра (
frame) во внутреннем пространстве View (.local) для точного позиционирования элементов внутри фигуры . - Глобальные координаты: Определение положения элемента относительно всего экрана (
.global), что необходимо для сложных анимаций перехода . - Избавление от «магических чисел»: Вместо жестко заданных смещений (например,
offset: 200), геометрия позволяет вычислить точное расстояние до края экрана для плавного исчезновения элементов .
Профессор вводит важное различие между «экраном» (Screen) и «сценой» (Scene) . В современной iOS приложение может работать в режиме многозадачности (split screen), поэтому полагаться на физические размеры устройства (UIScreen.main.bounds) — плохая практика. Вместо этого рекомендуется использовать GeometryReader на уровне всей сцены и передавать её параметры через EnvironmentValues (используя новый макрос @Entry) .
🖐️ Мультитач-жесты: от нажатий до вращения 22:44
SwiftUI берет на себя всю сложную математику распознавания касаний, позволяя разработчику сосредоточиться на обработке событий. Жесты разделяются на дискретные и непрерывные .
Типы жестов и способы их обработки:
- Дискретные жесты: Выполняются одномоментно (нажатие
TapGesture, долгое нажатиеLongPressGesture). Обрабатываются через замыкание.onEnded. - Непрерывные жесты: Сопровождаются изменением состояния во времени (
DragGesture,MagnifyGesture,RotationGesture). Требуют обработки через.onChanged, где передается параметрvalueс актуальными данными (скорость, угол, масштаб) .
В демо-примере RotationGesture используется для создания виртуального «диска» управления. При вращении двумя пальцами программа вычисляет угол в градусах, делит его на сегменты (по 90 градусов) и переключает выбранный цвет в игре . Профессор демонстрирует, как тестировать такие жесты в симуляторе Xcode, зажимая клавишу Option для появления двух точек касания .
🔍 Глубокое погружение в жесты увеличения (Pinch) 31:41
Для реализации изменения масштаба элементов списка (от компактного вида до подробного) используется MagnifyGesture. Профессор описывает важный паттерн управления состоянием при использовании таких жестов:
- Статическое состояние: Хранит значение, установленное после завершения последнего жеста (например, текущий режим размера) .
- Динамическое состояние: Хранит временный коэффициент масштабирования прямо в процессе движения пальцев .
- Итоговое значение: Вычисляемое свойство, которое перемножает статическое и динамическое значения .
Для элегантного переключения между режимами .compact, .regular и .large профессор перегружает оператор умножения * для перечисления (enum), что позволяет напрямую «умножать» размер интерфейса на коэффициент масштаба от жеста . Также демонстрируется использование AnyLayout для динамической смены контейнера с VStack на HStack при достижении критически малого размера .
📂 Файловая система и песочница (Sandbox) 48:40
Хотя SwiftData является основным способом сохранения данных, разработчикам часто нужно работать с файлами напрямую (например, для кэширования или обмена документами). Профессор объясняет концепцию «песочницы»: приложение имеет доступ только к строго определенным папкам своего Unix-контейнера в целях безопасности и приватности .
Структура песочницы приложения:
- Application Directory: Исполняемый файл и ресурсы (только для чтения) .
- Documents Directory: Данные, которые пользователь воспринимает как свои документы. Эта папка архивируется в iCloud .
- Application Support: Системные данные приложения, не являющиеся документами .
- Caches Directory: Временные файлы, которые не включаются в резервную копию и могут быть удалены системой при нехватке места .
Для работы с путями используется структура URL, а для чтения и записи — Data. Профессор предупреждает: метод Data(contentsOf:) блокирует основной поток, поэтому для сетевых или тяжелых файловых операций следует использовать асинхронный метод URLSession.shared.data(from:) с ключевым словом await .
🤖 Сериализация с Codable и JSON 1:00:21
Чтобы сохранить структуру или класс в файл, их нужно превратить в набор байтов. Протокол Codable объединяет Encodable и Decodable. Если все свойства типа уже являются «кодируемыми» (Int, String, Array и т.д.), Swift может автоматически синтезировать реализацию протокола .
Особенности реализации Codable для классов:
- При добавлении соответствия через расширение (
extension) инициализатор должен быть помечен какconvenience. - Класс должен быть помечен как
final, чтобы гарантировать корректность наследования инициализаторов . - Использование
JSONEncoderиJSONDecoderпозволяет легко преобразовывать объекты в JSON-строки и обратно .
В конце лекции кратко упоминается UserDefaults — хранилище типа «ключ-значение» для небольших настроек приложения . Профессор категорически не рекомендует использовать его для хранения сложных объектов, советуя в таких случаях использовать Codable и сохранять данные как Data .
🛠️ Практика: экспорт в JSON и загрузка из Bundle 1:10:57
В финальном демо профессор реализует кнопку «Сохранить», которая экспортирует текущее состояние игры в файл .json в папку Documents . Чтобы увидеть эти файлы вне приложения, необходимо добавить в Info.plist ключ Supports Document Browser (значение YES), после чего файлы станут доступны в системном приложении «Файлы» (Files) на iOS .
Вторая часть демо показывает, как загрузить «образцы игр» из ресурсов самого приложения (Bundle). Профессор использует Bundle.main.paths(forResourcesOfType:inDirectory:) для поиска всех JSON-файлов и асинхронно декодирует их при запуске приложения внутри блока Task . Это позволяет избежать блокировки интерфейса и корректно инициализировать начальные данные в SwiftData из внешних файлов .