Добавить объявление

Миграция в SwiftData: как обновлять модель без потери данных

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

Версионирование с первого дня:


Любая работа с данными должна начинаться с версионирования. Даже если у вас сейчас одна-единственная модель, ее стоит обернуть в VersionedSchema. Это создает стабильную точку отсчета. Потом, когда появятся изменения, будет понятно, откуда и куда мигрировать.

Обычно схемам дают номера версий: V1, V2, V3. В коде они живут как отдельные enum со списком моделей и идентификатором версии. А для удобства в основном коде используют typealias, чтобы каждый раз не писать ExerciseSchemaV5.Exercise.

Новую версию стоит заводить перед каждым релизом, в котором меняются модели. Даже если изменений несколько, они все могут войти в одну версию схемы. Главное чтобы пользователи, пропустившие пару обновлений, могли переехать сразу в актуальную версию без потери данных.

Когда SwiftData справляется сама:


Есть изменения, которые SwiftData умеет обрабатывать автоматически. К ним относятся:

  • Добавление опционального поля (старым объектам просто ставится nil).

  • Удаление поля (данные теряются, но миграция проходит гладко).

  • Переименование поля (если указать @Attribute(originalName:)).

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

Когда нужна ручная работа:


Легкая миграция перестает работать, если новое поле обязательное и без значения по умолчанию. SwiftData не может придумать, что туда записать. То же самое когда нужно преобразовать данные: изменить тип, разбить одно поле на несколько, смержить сущности, почистить дубликаты.

В таких случаях пишется SchemaMigrationPlan с кастомными этапами. У каждого этапа есть две фазы:

  • willMigrate - выполняется до применения новой схемы, работает со старыми данными (можно почистить дубликаты).

  • didMigrate - после применения, здесь уже доступны новые модели и можно заполнять поля.

Например, если в новой версии появилось обязательное поле createdAt, в didMigrate проходим по всем объектам и проставляем дату.

Сложные случаи - стратегия мостовой версии:


Иногда изменения настолько серьезные, что впихнуть все в одну миграцию рискованно. Например, в старой модели хранились вес, повторения и подходы в одном объекте, а в новой нужно разнести это по связанным сущностям.

Здесь выручает промежуточная версия. На первом шаге добавляем новые поля и отношения, но старые пока оставляем (можно переименовать, чтобы не конфликтовали). В миграции заполняем новые объекты из старых данных. А в следующей версии уже спокойно удаляем устаревшие поля - это будет легкая миграция.

Такой подход кажется более громоздким, но зато безопасным. Данные не теряются, каждый шаг тестируется отдельно.

Вывод:


Миграции в SwiftData - это не про героизм, а про аккуратность. Если с самого начала заложить версионирование и продумывать изменения, большинство проблем решаются либо автоматически, либо небольшими кастомными этапами. А для самых сложных случаев всегда можно разбить задачу на несколько версий, чтобы не переписывать все за один раз. Главное - не забывать тестировать на реальных данных, а не только на пустой базе в симуляторе.
20.03.2026 31 185