В тринадцатой лекции курса Stanford CS193p (весна 2025 года) профессор Пол Хэгарти приступает к одной из важнейших тем современной iOS-разработки — сохранению данных с помощью фреймворка SwiftData. Основная цель занятия — научить студентов делать данные приложения персистентными, чтобы информация не исчезала после закрытия программы или перезагрузки устройства . SwiftData представляет собой мощную надстройку над базой данных SQL, которая позволяет разработчикам работать на уровне привычных конструкций Swift, не погружаясь в сложности запросов к БД .
🛠️ Основы SwiftData: от классов к таблицам SQL 1:15
Переход к персистентной модели начинается с использования макроса @Model. По словам Хэгарти, этот макрос можно применять только к классам, поскольку именно они будут представлять собой таблицы в базе данных . Важной особенностью является то, что @Model автоматически делает класс наблюдаемым (Observable), поэтому использовать оба макроса одновременно не имеет смысла .
Поскольку данные сохраняются в SQL, на типы переменных внутри @Model накладываются определенные ограничения:
- Примитивные типы: числа, строки, булевы значения, даты и буферы данных (Data) .
- Другие
@Model: переменная может ссылаться на другой класс, помеченный этим же макросом, что создает связь между таблицами . - Структуры и перечисления (Enums): они должны соответствовать протоколу
Codable, чтобы SwiftData могла упаковать их в «бинарный мешок» для хранения . Однако профессор предупреждает: такие данные становятся непрозрачными для поиска внутри базы . - Массивы: допускаются массивы любых вышеперечисленных типов, но массивы других
@Modelвсегда будут неупорядоченными при извлечении из базы .
Хэгарти подчеркивает, что если тип данных не вписывается в эти рамки, разработчик может использовать вычисляемые свойства для преобразования данных в примитивные типы «на лету» .
🔗 Управление связями и атрибутами схемы 7:38
Структура базы данных описывается понятием «схема». Для настройки поведения полей используются атрибуты схемы. Например, атрибут .unique позволяет гарантировать уникальность значения (как ID сотрудника), а .externalStorage полезен для хранения тяжелых объектов, таких как видео или крупные изображения, вне основного файла базы данных .
Особое внимание Хэгарти уделяет управлению связями через @Relationship:
- Правила удаления (
deleteRule): профессор рекомендует использовать.cascade, чтобы при удалении основного объекта (например, игры) автоматически удалялись связанные с ним данные (попытки и коды) . - Инверсия (
inverse): позволяет SwiftData понимать двусторонние связи, когда дочерний объект имеет ссылку на родительский .
Для данных, которые нужны только во время работы приложения и не должны сохраняться на диск, предусмотрены маркеры .ephemeral или @Transient . Однако, по наблюдению Хэгарти, изменения @Transient переменных могут не обновлять пользовательский интерфейс автоматически, что требует ручной инициализации обновлений .
📑 Модель контекста и контейнера 10:01
Для взаимодействия с базой данных используются два ключевых компонента:
modelContext: это основной «инструмент» или дескриптор для добавления (insert), удаления (delete) и извлечения (fetch) объектов . Он также управляет транзакциями и функциями отмены (undo).modelContainer: контролирует физическое хранение данных (на диске или в памяти) и миграцию схем при обновлении версий приложения .
Хэгарти отмечает, что операции со SwiftData являются синхронными, но если приложение настроено на работу с iCloud, синхронизация с облаком происходит асинхронно в фоновом режиме, избавляя разработчика от необходимости писать сложный многопоточный код .
🔍 Искусство выборки данных: Predicates и @Query 14:45
Извлечение данных происходит с помощью FetchDescriptor, который определяет три вещи: тип искомой модели, условия поиска и порядок сортировки . Для задания условий используется макрос #Predicate. Это мощный инструмент, который превращает обычное замыкание Swift в сложный SQL-запрос .
Внутри предиктов можно использовать:
- Математические операции и сравнения.
- Логические операторы (
&&,||) и тернарные операторы . - Операции с последовательностями (
contains,allSatisfy) и точечную нотацию для переходов между связанными таблицами .
Несмотря на гибкость, предикты имеют ограничения: в них нельзя использовать вычисляемые свойства, так как база данных «видит» только реально хранящиеся поля .
В SwiftUI-представлениях профессор рекомендует использовать макрос @Query вместо ручного вызова fetch(). Это позволяет создать массив, который автоматически обновляется при любых изменениях в базе данных, работая аналогично @State или @Binding .
⚠️ Обработка ошибок в Swift 21:52
Работа с базой данных — это первый случай в курсе, когда функции могут возвращать ошибки (например, из-за повреждения файлов). Хэгарти объясняет стандартную систему Swift do-catch:
- Функции, способные выдать ошибку, помечаются словом
throwsи должны вызываться с префиксомtry. - Блок
catchпозволяет перехватить ошибку и обработать её (например, показать уведомление пользователю) . - Существуют также
try!(вызывает фатальную ошибку при неудаче) иtry?(возвращаетnilвместо ошибки, что удобно для простых проверок) .
👨💻 Демо: адаптация модели CodeBreaker под SwiftData 31:30
Практическая часть занятия посвящена конвертации существующей игры CodeBreaker. Основная сложность заключается в том, что типы данных в модели не всегда совместимы с SQL. Например, тип Color не является примитивным и не может быть сохранен напрямую .
Хэгарти демонстрирует «массаж данных» (data massaging):
- Превращение структур в классы:
Codeстановится@Model class, что требует добавления инициализатора, так как классы не получают его автоматически, в отличие от структур . - Проблема ссылок: Хэгарти указывает на опасный баг — при переходе от структур к классам простое присваивание перестает копировать данные, создавая лишь вторую ссылку на тот же объект в куче . Это требует явного создания копий объектов.
- Использование ИИ: Профессор обращается к ChatGPT, чтобы быстро сгенерировать код для перечисления
Kind, которое содержит ассоциативные данные и не может быть автоматически преобразовано в строку . Хэгарти выбирает хранение этого перечисления в видеStringв базе данных, чтобы сохранить возможность поиска по типу . - UI-независимость: Хэгарти делает модель полностью независимой от SwiftUI, заменяя
ColorнаString(Hex-коды). Чтобы не ломать UI-код, он добавляет расширения с вычисляемыми свойствами, которые прозрачно конвертируют строки обратно в цвета для представлений .
В завершение лекции в главный файл приложения добавляется .modelContainer(for: CodeBreaker.self) . Профессор демонстрирует, что после всех манипуляций игра продолжает работать, а данные теперь готовы к полноценному сохранению, которое будет реализовано на следующем занятии .