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-проектами.
Полезные материалы
ссылка на оригинал статьи https://habr.com/ru/articles/753552/
Добавить комментарий