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