Swift Data: Новый ORM от Apple для Swift
1. Что это и зачем нужна
Swift Data — это современный, легковесный ORM (Object-Relational Mapping) фреймворк, представленный Apple на WWDC 2023. Он предназначен для работы с постоянными данными в приложениях на Swift, поддерживающих iOS 17+, macOS 14+, tvOS 17+ и watchOS 10+. Swift Data пришёл на смену устаревшему Core Data, предлагая более простой, декларативный и типобезопасный подход к управлению базой данных.
Зачем нужен Swift Data? В первую очередь, для значительного упрощения кода. Вместо ручного создания NSManagedObject, настройки контекстов и написания сложных запросов на Swift, разработчик описывает модель данных с помощью обычных Swift-классов и макросов (@Model, @Attribute, @Relationship). Swift Data автоматически генерирует всю необходимую "подкапотную" логику: создание таблиц, управление изменениями, отслеживание зависимостей и синхронизацию с UI через SwiftUI.
Ключевое преимущество — тесная интеграция с SwiftUI. Изменения в данных автоматически обновляют интерфейс без необходимости вручную вызывать objectWillChange.send() или перезагружать списки. Это делает Swift Data идеальным выбором для новых проектов на SwiftUI, где требуется локальное хранение данных (списки задач, заметки, настройки, кэш).
2. Установка
Swift Data является частью стандартной библиотеки Apple, начиная с iOS 17 и macOS 14. Отдельная установка через менеджеры пакетов (pip, npm, cargo) не требуется. Для использования достаточно импортировать фреймворк в файле Swift.
Важно: Минимальная версия развертывания (Deployment Target) вашего проекта должна быть iOS 17.0 или macOS 14.0.
Команда для добавления в проект (через Xcode):
// Swift Data не требует установки. Просто импортируйте:
import SwiftData
Если вы используете Swift Package Manager (SPM) для управления зависимостями, Swift Data не нужно добавлять как отдельный пакет — он встроен в SDK.
3. Быстрый старт — минимальный рабочий пример
Создадим простое приложение для хранения списка задач (To-Do).
import SwiftUI
import SwiftData
// 1. Определяем модель данных
@Model
final class TaskItem {
var title: String
var isCompleted: Bool
var createdAt: Date
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
self.createdAt = Date()
}
}
// 2. Настраиваем контейнер в App
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: TaskItem.self) // Автоматически создает контейнер
}
}
// 3. Используем данные в View
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query private var tasks: [TaskItem]
var body: some View {
NavigationStack {
List {
ForEach(tasks) { task in
HStack {
Text(task.title)
Spacer()
if task.isCompleted {
Image(systemName: "checkmark")
}
}
}
.onDelete(perform: deleteTasks)
}
.navigationTitle("Задачи")
.toolbar {
ToolbarItem {
Button("Добавить") {
let newTask = TaskItem(title: "Новая задача \(tasks.count + 1)")
context.insert(newTask)
}
}
}
}
}
func deleteTasks(at offsets: IndexSet) {
for index in offsets {
context.delete(tasks[index])
}
}
}
Этот код создает полноценное приложение для работы со списком задач с поддержкой добавления, удаления и автоматического сохранения.
4. Основные методы, функции и классы
4.1. @Model (Макрос)
Сигнатура: @Model — макрос, применяемый к классу.
Что делает: Преобразует обычный Swift-класс в управляемую модель данных Swift Data. Автоматически генерирует хранилище, отслеживание изменений и конформность протоколу PersistentModel.
@Model
final class User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
4.2. @Attribute (Макрос)
Сигнатура: @Attribute(.unique) — применяется к свойству модели.
Что делает: Позволяет настроить поведение атрибута: сделать его уникальным, задать правило удаления (каскадное), указать имя в хранилище и т.д.
@Model
final class Product {
@Attribute(.unique) var sku: String // Уникальный идентификатор
var name: String
init(sku: String, name: String) {
self.sku = sku
self.name = name
}
}
4.3. @Relationship (Макрос)
Сигнатура: @Relationship(deleteRule: .cascade, inverse: \Child.parent)
Что делает: Определяет связи между моделями (один-к-одному, один-ко-многим, многие-ко-многим). Позволяет настроить правило удаления (каскадное, nullify, deny).
@Model
final class Author {
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book] = []
}
@Model
final class Book {
var title: String
var author: Author?
}
4.4. ModelContainer (Класс)
Сигнатура: ModelContainer(for: [Any.Type]) или ModelContainer(for: [Any.Type], configurations: [ModelConfiguration])
Что делает: Создает и управляет базой данных (по умолчанию SQLite). Является точкой входа для всех операций с данными. Обычно создается один раз в App.
// Создание контейнера для одной модели
let container = try ModelContainer(for: TaskItem.self)
// Создание контейнера для нескольких моделей с кастомной конфигурацией
let config = ModelConfiguration(url: URL.documentsDirectory.appending(path: "myapp.sqlite"))
let container = try ModelContainer(for: [User.self, Product.self], configurations: config)
4.5. ModelContext (Класс)
Сигнатура: context.insert(_ model: PersistentModel), context.delete(_ model: PersistentModel), try context.save()
Что делает: Предоставляет "рабочее пространство" для создания, изменения и удаления объектов. Все изменения временны, пока не будет вызван save(). В SwiftUI доступен через @Environment(\.modelContext).
struct ContentView: View {
@Environment(\.modelContext) private var context
func addUser() {
let user = User(name: "Alice", age: 30)
context.insert(user) // Добавляем в контекст
try? context.save() // Сохраняем в базу
}
}
4.6. @Query (Свойство-обертка)
Сигнатура: @Query(sort: \.createdAt, order: .reverse, animation: .default) var items: [ItemType]
Что делает: Автоматически извлекает данные из базы и обновляет View при любых изменениях. Поддерживает сортировку, фильтрацию (через Predicate) и анимацию.
struct TasksView: View {
// Сортировка по дате создания, только невыполненные задачи
@Query(filter: #Predicate { $0.isCompleted == false },
sort: \.createdAt, order: .reverse)
var pendingTasks: [TaskItem]
var body: some View {
List(pendingTasks) { task in
Text(task.title)
}
}
}
4.7. #Predicate (Макрос)
Сигнатура: #Predicate<ModelType> { $0.property == value }
Что делает: Создает типобезопасные предикаты для фильтрации запросов. Позволяет использовать сложные условия (AND, OR, сравнение строк).
let predicate = #Predicate<User> { user in
user.age > 18 && user.name.contains("A")
}
@Query(filter: predicate) var adultUsers: [User]
4.8. FetchDescriptor (Структура)
Сигнатура: FetchDescriptor<T>(predicate: Predicate<T>?, sortBy: [SortDescriptor<T>])
Что делает: Используется для выполнения кастомных запросов, когда @Query недостаточно (например, для пагинации, подсчета количества записей). Работает через context.fetch().
func fetchRecentUsers() -> [User] {
let descriptor = FetchDescriptor<User>(
predicate: #Predicate { $0.age > 25 },
sortBy: [SortDescriptor(\.name)]
)
descriptor.fetchLimit = 10
let results = try? context.fetch(descriptor)
return results ?? []
}
4.9. ModelConfiguration (Структура)
Сигнатура: ModelConfiguration(schema: Schema?, storageURL: URL?, cloudKitContainerIdentifier: String?)
Что делает: Позволяет настроить хранилище: указать кастомный URL для SQLite файла, включить синхронизацию с iCloud через CloudKit, задать миграции.
let config = ModelConfiguration(
schema: Schema([TaskItem.self]),
storageURL: URL.documentsDirectory.appending(path: "tasks.sqlite"),
cloudKitContainerIdentifier: "iCloud.com.example.myapp"
)
4.10. Schema.MigrationPlan (Тип)
Сигнатура: Schema.MigrationPlan.stages([MigrationStage])
Что делает: Определяет план миграции базы данных при изменении модели. Позволяет выполнять кастомные скрипты для переноса данных между версиями.
enum MyMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [SchemaV1.self, SchemaV2.self]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
5. Реальный практический пример (100-200 строк кода)
Создадим полноценное приложение "Менеджер проектов" с задачами, категориями и поиском.
import SwiftUI
import SwiftData
// MARK: - Модели данных
@Model
final class Project {
@Attribute(.unique) var name: String
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task] = []
init(name: String) {
self.name = name
self.createdAt = Date()
}
}
@Model
final class Task {
var title: String
var isCompleted: Bool
var priority: Int
var project: Project?
init(title: String, priority: Int = 0) {
self.title = title
self.priority = priority
self.isCompleted = false
}
}
// MARK: - Приложение
@main
struct ProjectManagerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Project.self, Task.self])
}
}
// MARK: - Основной экран
struct ContentView: View {
@Environment(\.modelContext) private var context
@State private var searchText = ""
var body: some View {
NavigationStack {
ProjectListView(searchText: searchText)
.searchable(text: $searchText)
.navigationTitle("Проекты")
.toolbar {
ToolbarItem {
Button("+ Проект") {
let project = Project(name: "Проект \(Int.random(in: 1...100))")
context.insert(project)
try? context.save()
}
}
}
}
}
}
// MARK: - Список проектов
struct ProjectListView: View {
let searchText: String
@Query private var projects: [Project]
init(searchText: String) {
self.searchText = searchText
if searchText.isEmpty {
_projects = Query(sort: \.createdAt, order: .reverse)
} else {
let predicate = #Predicate<Project> { $0.name.localizedStandardContains(searchText) }
_projects = Query(filter: predicate, sort: \.createdAt, order: .reverse)
}
}
var body: some View {
List {
ForEach(projects) { project in
NavigationLink(destination: TaskListView(project: project)) {
VStack(alignment: .leading) {
Text(project.name).font(.headline)
Text("\(project.tasks.count) задач")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.onDelete(perform: deleteProjects)
}
}
func deleteProjects(at offsets: IndexSet) {
for Также в библиотеке
Redis клиент для Java: Jedis, Lettuce и Redisson — обзор и примеры
OAuth клиент для TypeScript: интеграция авторизации в веб-приложениях
once_cell для Rust: полное руководство с примерами
Работа с Markdown в Go (Golang): библиотеки, парсинг и конвертация