SwiftData: быстрый старт

от автора

Apple представила на WWDC23 большое количество новых вещей для разработки. Хранения данных — не исключение. SwiftData — это новый фреймворк для работы с хранением данных внутри приложения, который представляет собою новый уровень абстракции над уже существующем механизмом — CoreData. В данной статье будут описаны основные этапы работы с SwiftData, а именно:

  1. Создание моделей

  2. Установка атрибутов свойств

  3. Описание связей между таблицами

  4. Создание контейнера моделей

  5. Описание миграций

  6. Использование в сценариях

  7. Заключение

Создание моделей

Для начала работы со 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 были добавлены атрибуты, которые можно применять для свойств в модели.

  1. Unique — гарантирует, что поле будет актуальным для моделей, которые хранятся. Если уже существуют модели с не уникальными значениями, то необходимо в рамках миграции дедуплицировать модели.

@Attrbitute(.unique) var id: UUID 
  1. Encrypt — сохраняет значение свойства в зашифрованном типе

@Attribute(.ecrypt) var somethingImportant: String 
  1. Spotlight — включает индексацию свойства, для доступа в Spotlight(аналогично переключателю включения индексации в редакторе схемы)

@Attrbitute(.spotlight) var productName: String 
  1. ExternalStorage -сохраняет значение свойства в виде двоичных данных рядом с хранилищем модели.

@Attrbitute(.externalStorage) var profileImage: UIImage 
  1. Transient -временное поле, которое не участвует в генерации схемы и не будет записано в БД.

@Transient var isSelected: Bool = true  

После описания модели и атрибутов ее свойств, идёт описание связей между моделями.

Выстраивание связей между моделями внутри таблиц

Макрос @Relationships — еще одна новинка, которая позволяет описать правила удаления:

  1. Cascade — удаляет все связанные модели. Это правило в основном используется, когда модель данных имеет большую зависимость. Оно удалит все ваши записи без каких-либо указаний, если ваш объект связи будет удален.

@Relationships(.cascade) var relatives: [User] 
  1. Nullify — зануляет ссылку на модель, с которой была связь.

@Relationships(.nullify) var relatives: [User] 
  1. Deny — предотвращает удаление модели, поскольку она содержит одну или несколько ссылок на другие модели. Можно сказать, что полная противоположность работы с каскадным удалением.

@Relationships(.deny) var relatives: [User] 
  1. NoAction — не вносит изменений ни в какие связанные модели. Однако Apple в документации явно говорит, что:

Убедитесь что вы выполняете соответствующие действия со всеми связанными моделями при использовании этого правила удаления, например, удаляете их или аннулируете их ссылки на удаленную модель. В противном случае ваши данные будут в несогласованном состоянии и могут ссылаться на несуществующие модели. (оригинал)

Создание контейнера моделей

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

  1. Отслеживание обновлений данных

  2. Извлечение данных

  3. Сохранение данных

  4. Откат изменений

Существует два способа создания контейнера:

  1. Простой. Для создание контейнера необходимо передать список моделей (или одну модель), схему для которой необходимо сконфигурировать.

// Самый простой способ создать контейнер, описав модели,  // которые необходимы для построения схемы let container = try ModelContainer(     for: [Trip.self, Person.self] ) 
  1. Чуть сложнее: Необходимо передать модели для генерации схемы а также ее конфигурацию. Она включает в себя:

    1. Работа в inMemory режиме

    2. Необходимость интеграции с CloudKit

    3. Работа в режиме ReadOnly

// Пример создания контейнера с конфигурацией let configuration = ModelConfiguration( inMemory: true,  readOnly: true ) let container = try ModelContainer(     for: [Trip.self, Person.self],      configurations: configuration ) 

Описание миграций

Любое приложение развивается и хранилище данных вместе с ним. Для того чтобы обновить модель данных для нынешних пользователей приложения, необходимо проводить миграцию данных.
Существует два основных вида миграции:

  1. Легковесная(автоматическая) миграция (например, расширение контракта, перевод неопционального поля в опциональный)

  2. Ручная миграция (например, установка атрибута unique, изменение типа данных) Для того чтобы применить миграции, необходимо создать перечисление, который будет соответствовать протоколу SchemaMigrationPlan. Для реализации протокола необходимо указать:

  3. Массив schemas, который содержит перечисление всех Схем приложения, которые были созданы.

  4. Массив stages, который содержит реализации миграций:

    1. Легковесная миграция

static let migrateVltoV2 = MigrationStage.lightweight( fromVersion: PeopleSchemaV1.self, toVersion: PeopleSchemaV2.self ) 
  1. Ручная миграция

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 // Исполняемый код } ) } 
  1. Необходимо добавить план миграции в конфигурацию контейнера.

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 принимает в себя:

  1. Новый предикат (типизированный, через конструктор)

  2. Порядок сортировки

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-проектами.

Полезные материалы

  1. Meet SwiftData—WWDC 2023

  2. Build an App with SwiftData — WWDC 2023

  3. Migrate to SwiftData — WWDC 2023

  4. Проект с примерами использования SwiftData


ссылка на оригинал статьи https://habr.com/ru/articles/753552/