Apple представила на WWDC23 большое количество новых вещей для разработки. Хранения данных — не исключение. SwiftData - это новый фреймворк для работы с хранением данных внутри приложения, который представляет собою новый уровень абстракции над уже существующем механизмом - CoreData. В данной статье будут описаны основные этапы работы с SwiftData, а именно:
Создание моделей
Установка атрибутов свойств
Описание связей между таблицами
Создание контейнера моделей
Описание миграций
Использование в сценариях
Заключение
Создание моделей
Для начала работы со SwiftData, как и для работы с CoreData, необходимо создать модели данных, которые нужно хранить.
Работая в CoreDara, создавать и выстраивать связей между моделями не кажется самой удобной вещью, так как необходимо было вручную прописывать и править информацию в xcdatamodeld файле

С приходом SwiftData стало возможным описывать все модели и связи между ними используя новый макрос: @Model
@Model final class Person { var firstName: String var lastName: String var dateOfBirth: Date var relatives: [Person] = [] }
@Model позволяет генерировать схему согласно тем свойствам, которые будут описаны в рамках модели. Прямо из коробки существует поддержка таких типов, как структуры, коллекций (только value type), а также модели, которые соответствуют протоколу Codable. В момент генерации через макрос добавляется поддержка двух протоколов: PersistentModel (для генерации схемы) и известный протокол Observable(для подписки на обновления модели)

Установка атрибутов свойств
В SwiftData были добавлены атрибуты, которые можно применять для свойств в модели.
Unique - гарантирует, что поле будет актуальным для моделей, которые хранятся. Если уже существуют модели с не уникальными значениями, то необходимо в рамках миграции дедуплицировать модели.
@Attrbitute(.unique) var id: UUID
Encrypt - сохраняет значение свойства в зашифрованном типе
@Attribute(.ecrypt) var somethingImportant: String
Spotlight - включает индексацию свойства, для доступа в Spotlight(аналогично переключателю включения индексации в редакторе схемы)
@Attrbitute(.spotlight) var productName: String
ExternalStorage -сохраняет значение свойства в виде двоичных данных рядом с хранилищем модели.
@Attrbitute(.externalStorage) var profileImage: UIImage
Transient -временное поле, которое не участвует в генерации схемы и не будет записано в БД.
@Transient var isSelected: Bool = true
После описания модели и атрибутов ее свойств, идёт описание связей между моделями.
Выстраивание связей между моделями внутри таблиц
Макрос @Relationships - еще одна новинка, которая позволяет описать правила удаления:
Cascade - удаляет все связанные модели. Это правило в основном используется, когда модель данных имеет большую зависимость. Оно удалит все ваши записи без каких-либо указаний, если ваш объект связи будет удален.
@Relationships(.cascade) var relatives: [User]
Nullify - зануляет ссылку на модель, с которой была связь.
@Relationships(.nullify) var relatives: [User]
Deny - предотвращает удаление модели, поскольку она содержит одну или несколько ссылок на другие модели. Можно сказать, что полная противоположность работы с каскадным удалением.
@Relationships(.deny) var relatives: [User]
NoAction - не вносит изменений ни в какие связанные модели. Однако Apple в документации явно говорит, что:
Убедитесь что вы выполняете соответствующие действия со всеми связанными моделями при использовании этого правила удаления, например, удаляете их или аннулируете их ссылки на удаленную модель. В противном случае ваши данные будут в несогласованном состоянии и могут ссылаться на несуществующие модели. (оригинал)
Создание контейнера моделей
Теперь поговорим про то, как можно взаимодействовать с данными через SwiftData. ModelContainer управляет схемой приложения и конфигурацией хранилища моделей. Кроме этого контейнер отслеживает все изменения в моделях и предоставляет большое количество действий для работы с ними, а именно:
Отслеживание обновлений данных
Извлечение данных
Сохранение данных
Откат изменений
Существует два способа создания контейнера:
Простой. Для создание контейнера необходимо передать список моделей (или одну модель), схему для которой необходимо сконфигурировать.
// Самый простой способ создать контейнер, описав модели, // которые необходимы для построения схемы let container = try ModelContainer( for: [Trip.self, Person.self] )
Чуть сложнее: Необходимо передать модели для генерации схемы а также ее конфигурацию. Она включает в себя:
Работа в inMemory режиме
Необходимость интеграции с CloudKit
Работа в режиме ReadOnly
// Пример создания контейнера с конфигурацией let configuration = ModelConfiguration( inMemory: true, readOnly: true ) let container = try ModelContainer( for: [Trip.self, Person.self], configurations: configuration )
Описание миграций
Любое приложение развивается и хранилище данных вместе с ним. Для того чтобы обновить модель данных для нынешних пользователей приложения, необходимо проводить миграцию данных.
Существует два основных вида миграции:
Легковесная(автоматическая) миграция (например, расширение контракта, перевод неопционального поля в опциональный)
Ручная миграция (например, установка атрибута unique, изменение типа данных) Для того чтобы применить миграции, необходимо создать перечисление, который будет соответствовать протоколу
SchemaMigrationPlan. Для реализации протокола необходимо указать:Массив
schemas, который содержит перечисление всех Схем приложения, которые были созданы.Массив
stages, который содержит реализации миграций:Легковесная миграция
static let migrateVltoV2 = MigrationStage.lightweight( fromVersion: PeopleSchemaV1.self, toVersion: PeopleSchemaV2.self )
Ручная миграция
static let migrateV2toV3 = MigrationStage.custom( fromVersion: PeopleSchemaV2.self, toVersion: PeopleSchemaV3.self, willMigrate: { _ in // Исполняемый код }, didMigrate: { _ in // Исполняемый код } )
В итоге должен получиться следующий код:
enum SamplePeopleMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [ PeopleSchemaV1.self, PeopleSchemaV2.self, PeopleSchemaV3.self ] } static var stages: [MigrationStage] { [ migrateV1toV2, migrateV2toV3 ] } static let migrateVltoV2 = MigrationStage.lightweight( fromVersion: PeopleSchemaV1.self, toVersion: PeopleSchemaV2.self ) static let migrateV2toV3 = MigrationStage.custom( fromVersion: PeopleSchemaV2.self, toVersion: PeopleSchemaV3.self, willMigrate: { _ in // Исполняемый код }, didMigrate: { _ in // Исполняемый код } ) }
Необходимо добавить план миграции в конфигурацию контейнера.
let container = ModelContainer( for: Person.self, migrationPlan: SamplePeopleMigrationPlan.self )
Использование в сценариях
Для того чтобы получить данные достаточно, использовать новый макрос Query. Обычный сценарий: стартовый экран со списком элементов, который необходимо наполнить данными, согласно схеме.
import SwiftData struct FamilyList: View { @Query( sort: \.lastName, filter: { $0.firstName > $1.firstName }, order: .reverse ) private var people: [Person] @Environment(\.modelContext) private var modelContext var body: some View { NavigationStack { List { ForEach(people, id: \.id) { person in Text(person.name) } .onDelete(perform: deleteRelative) } .navigationTitle(Constants.title) .toolbar { Button(action: addRelative) { Text(Constants.toolbarTitle) } } } } }
Для доступа к данным из контекста есть возможность обратиться через FetchDescriptor (iOS 17.0+), который содержит дженерик, удовлетворяющий протокол PersistentModel.
FetchDescriptor принимает в себя:
Новый предикат (типизированный, через конструктор)
Порядок сортировки
let context = container.mainContext let maturePeople = FetchDescriptor<Person>( predicate: #Predicate { $0.ages > 20 }, sort: \.age ) maturePeople = 10 let results = context.fetch(maturePeople)
Для сохранения/удаления/изменения необходимо получить инстанс из контекста. При выключенном автосохранении необходимо, как и при использовании CoreData, вызывать метод save() в контексте.
Заключение
При первой прикидке кажется, что релиз SwiftData поспособствует понижению порога входа для использования CoreData. К тому же добавит больше безопасности в работу с хранением данных, извлечением, в тоже время оставит возможность для сложной работы с различными видами контекстов и продвинутыми режимами работы. Но при всех очевидных плюсах закрался один существенный недостаток: минимальная версия начинается с iOS 17, что делает использование SwiftData непозволительно дорогим для крупных приложений, но почти идеальным решением для работы над различными pet-проектами.
