SwiftData от основите до продукция: Пълно ръководство за модерния слой за данни в Swift
С пускането на iOS 17, Apple направи нещо, което Swift разработчиците чакаха с години — представи SwiftData, изцяло нов фреймуърк за съхранение на данни. И не, той не е просто обвивка около Core Data. SwiftData е преосмислен от нулата — проектиран да се интегрира безпроблемно със SwiftUI и да използва пълната мощ на макросите в Swift 5.9.
Честно казано, ако сте работили с Core Data, знаете болките. XML файлове за модела на данни, NSManagedObject подкласове, NSFetchRequest с неговите предикати базирани на стрингове, NSPersistentContainer конфигурация... и цялата церемониалност, която идва с това.
SwiftData елиминира всичко това. Вместо да дефинирате модел в графичен редактор, просто пишете Swift класове с макрото @Model. Вместо NSFetchRequest, използвате #Predicate — типово безопасен и проверяван от компилатора. Вместо NSPersistentContainer, имате ModelContainer, който се конфигурира с един ред код. Доста голяма разлика, нали?
В това ръководство ще разгледаме SwiftData изчерпателно — от базовите концепции и дефиниране на модели, през CRUD операции и заявки, до миграция на схемата, новата поддръжка на наследяване на класове в iOS 26 и най-добрите практики за продукционен код. Ако сте iOS разработчик, който иска да изгради стабилен слой за данни — това ръководство е точно за вас.
Основни компоненти на SwiftData
Преди да се потопим в кода, нека разберем четирите основни компонента, които изграждат архитектурата на SwiftData.
@Model — Макрото за дефиниране на модели
Макрото @Model е входната точка към SwiftData. Когато го приложите върху клас, компилаторът автоматично генерира целия необходим код за съхранение — подобно на това, което преди правехте ръчно с NSManagedObject. Класът автоматично получава съответствие с протокола PersistentModel и всички свойства стават автоматично следени за промени. С две думи — магия зад кулисите.
ModelContainer — Контейнерът за данни
ModelContainer е отговорен за цялата конфигурация на хранилището — къде се записват данните, коя схема се използва, дали хранилището е в паметта или на диска. Мислете за него като значително по-опростен наследник на NSPersistentContainer.
ModelContext — Контекстът за работа с данни
ModelContext е средата, в която извършвате всички операции с данни — създаване, четене, обновяване и изтриване. Той следи промените и ги записва в хранилището. В SwiftUI получавате достъп до него чрез @Environment(\.modelContext).
@Query — Декларативни заявки в SwiftUI
Property wrapper-ът @Query замества @FetchRequest от Core Data. Той автоматично извлича данни от хранилището и поддържа изгледа актуален при промени. Сортиране, филтриране, анимации — всичко декларативно и с минимален код.
Дефиниране на модели
Тук нещата стават наистина приятни. Дефинирането на модели в SwiftData е драматично по-просто от Core Data. Няма .xcdatamodeld файл, няма графичен редактор, няма генериране на подкласове. Просто пишете обикновени Swift класове и добавяте макрото @Model.
Базов модел с @Model
import SwiftData
@Model
final class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
@Relationship(deleteRule: .cascade) var subtasks: [Subtask]
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
self.createdAt = Date()
}
}
@Model
final class Subtask {
var title: String
var isCompleted: Bool
var task: Task?
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
Забележете колко чист е този код. Няма NSManagedObject, няма @NSManaged, няма optional типове навсякъде. Просто стандартни Swift свойства с ясни типове. Макрото @Model автоматично прави всяко съхраняемо свойство наблюдаемо и го регистрира в схемата на SwiftData. Ако идвате от Core Data, вероятно вече се чувствате облекчени.
Работа с @Attribute
Макрото @Attribute ви дава фин контрол над начина, по който отделните свойства се съхраняват. Можете да зададете уникалност, да изключите свойства от записване или да конфигурирате трансформации. Ето пример:
import SwiftData
@Model
final class User {
// Уникално свойство — не може да има двама потребители с един и същ имейл
@Attribute(.unique) var email: String
var name: String
var bio: String
// Изключено от запис в хранилището — изчислява се динамично
@Attribute(.ephemeral) var isOnline: Bool
// Съхранение на големи данни като външен файл
@Attribute(.externalStorage) var profileImage: Data?
// Оригиналното име на свойството в базата данни
@Attribute(originalName: "user_name") var username: String
init(email: String, name: String, username: String, bio: String = "") {
self.email = email
self.name = name
self.username = username
self.bio = bio
self.isOnline = false
}
}
Атрибутът .unique е изключително полезен — той гарантира целостта на данните на ниво хранилище. Ако се опитате да вмъкнете обект с дублиран уникален атрибут, SwiftData ще извърши upsert операция — ще обнови съществуващия запис вместо да създаде дубликат. А .externalStorage? Той казва на SwiftData да съхрани данните като външен файл, което е идеално за снимки и други тежки блокове данни (и реално подобрява производителността на заявките).
Поддържани типове данни
SwiftData поддържа широк набор от типове данни без необходимост от трансформатори:
- Примитивни типове:
String,Int,Double,Float,Bool - Дати и данни:
Date,Data - URL и UUID:
URL,UUID - Колекции:
Array,Dictionary,Set(с поддържани елементи) Codableстойности — автоматично се сериализиратenumтипове, които саCodableиRawRepresentable- Опционални версии на всички горепосочени типове
// Пример с enum и Codable структура
enum Priority: Int, Codable {
case low = 0
case medium = 1
case high = 2
case critical = 3
}
struct Address: Codable {
var street: String
var city: String
var postalCode: String
}
@Model
final class Project {
var name: String
var priority: Priority
var tags: [String]
var metadata: [String: String]
var address: Address?
init(name: String, priority: Priority = .medium) {
self.name = name
self.priority = priority
self.tags = []
self.metadata = [:]
}
}
Настройка на ModelContainer
Преди да можете да работите с данни, трябва да конфигурирате ModelContainer. Най-простият начин в SwiftUI приложение е чрез модификатора .modelContainer:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Task.self, Subtask.self])
}
}
Този един ред код създава хранилище на диска, регистрира посочените модели и автоматично инжектира ModelContext в средата на SwiftUI. Впечатляващо, нали? Но за продукционни приложения обикновено ще ви трябва повече контрол:
@main
struct MyApp: App {
let container: ModelContainer
init() {
let schema = Schema([
Task.self,
Subtask.self,
User.self,
Project.self
])
let config = ModelConfiguration(
"MainStore",
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
container = try ModelContainer(
for: schema,
configurations: [config]
)
} catch {
fatalError("Неуспешно създаване на ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
За тестове и визуализации в Xcode Previews е изключително удобно да използвате хранилище в паметта. Лично аз го правя постоянно — спестява страшно много време:
// Хранилище в паметта за тестове и визуализации
let previewContainer: ModelContainer = {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Task.self, Subtask.self,
configurations: config
)
// Зареждане на примерни данни
let context = container.mainContext
let sampleTask = Task(title: "Примерна задача")
context.insert(sampleTask)
return container
}()
#Preview {
TaskListView()
.modelContainer(previewContainer)
}
CRUD операции
CRUD операциите в SwiftData са интуитивни и директни. Няма нужда от специални методи за създаване на обекти или сложни fetch заявки за четене. Нека разгледаме всяка операция.
Създаване (Create)
struct AddTaskView: View {
@Environment(\.modelContext) private var context
@State private var title = ""
@State private var priority: Priority = .medium
var body: some View {
Form {
TextField("Заглавие на задачата", text: $title)
Picker("Приоритет", selection: $priority) {
Text("Нисък").tag(Priority.low)
Text("Среден").tag(Priority.medium)
Text("Висок").tag(Priority.high)
}
Button("Създай задача") {
// Създаване на нов обект
let newTask = Task(title: title)
// Вмъкване в контекста
context.insert(newTask)
// Данните се записват автоматично, но може и ръчно
try? context.save()
title = ""
}
}
}
}
Важно е да се знае, че SwiftData поддържа автоматичен запис. При стандартна конфигурация контекстът записва промените автоматично в определени моменти — например при излизане от преден план. Въпреки това, за критични данни си струва да извикате context.save() ръчно. По-добре да сте сигурни.
Четене (Read)
За четене на данни в SwiftUI изгледи използвайте @Query:
struct TaskListView: View {
@Query(sort: \Task.createdAt, order: .reverse)
private var tasks: [Task]
@Environment(\.modelContext) private var context
var body: some View {
NavigationStack {
List(tasks) { task in
TaskRow(task: task)
}
.navigationTitle("Задачи")
.overlay {
if tasks.isEmpty {
ContentUnavailableView(
"Няма задачи",
systemImage: "checklist",
description: Text("Създайте първата си задача")
)
}
}
}
}
}
А за програмно четене извън SwiftUI изгледи — FetchDescriptor:
// Извличане на всички незавършени задачи
func fetchPendingTasks(context: ModelContext) throws -> [Task] {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.isCompleted },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return try context.fetch(descriptor)
}
// Извличане с лимит — само първите 10 резултата
func fetchRecentTasks(context: ModelContext) throws -> [Task] {
var descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
descriptor.fetchLimit = 10
return try context.fetch(descriptor)
}
Обновяване (Update)
Обновяването в SwiftData е може би най-приятната изненада — просто променяте свойствата на обекта и толкова. SwiftData автоматично проследява промените и ги синхронизира с хранилището. Без допълнителни стъпки.
struct TaskRow: View {
let task: Task
@Environment(\.modelContext) private var context
var body: some View {
HStack {
Button {
// Обновяването е просто присвояване на нова стойност
task.isCompleted.toggle()
// Промяната се проследява автоматично
} label: {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
}
VStack(alignment: .leading) {
Text(task.title)
.strikethrough(task.isCompleted)
Text(task.createdAt, style: .date)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
Изтриване (Delete)
struct TaskListView: View {
@Query(sort: \Task.createdAt, order: .reverse)
private var tasks: [Task]
@Environment(\.modelContext) private var context
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
// Изтриване на обект от контекста
context.delete(tasks[index])
}
// Записване на промените
try? context.save()
}
}
// Групово изтриване на всички завършени задачи
func deleteCompletedTasks(context: ModelContext) throws {
try context.delete(
model: Task.self,
where: #Predicate { $0.isCompleted }
)
try context.save()
}
Методът context.delete(model:where:) заслужава специално внимание — той работи директно на ниво хранилище, без да зарежда обектите в паметта. Това е значително по-ефективно от зареждане и изтриване един по един, особено когато имате стотици или хиляди записи.
Заявки и предикати
Системата за заявки в SwiftData е едно от най-значителните подобрения спрямо Core Data. И тук наистина се вижда колко добре е обмислен фреймуъркът.
Макрото #Predicate
#Predicate е макро, което преобразува Swift изрази в предикати, които SwiftData може да изпълни на ниво хранилище. Компилаторът проверява типовете и имената на свойствата, така че грешки от рода на грешно изписано име на свойство просто не могат да се случат. Край на грешките с NSPredicate базирани на стрингове (и добре, че е така).
// Търсене по текст
let searchTerm = "groceries"
let predicate = #Predicate { task in
task.title.localizedStandardContains(searchTerm) &&
!task.isCompleted
}
let descriptor = FetchDescriptor(
predicate: predicate,
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
let results = try context.fetch(descriptor)
Комбиниране на предикати
// Сложен предикат с множество условия
func buildTaskPredicate(
searchText: String,
showCompleted: Bool,
minimumPriority: Priority
) -> Predicate {
let minPriorityRaw = minimumPriority.rawValue
return #Predicate { task in
// Филтриране по текст (ако има въведен)
(searchText.isEmpty || task.title.localizedStandardContains(searchText))
&&
// Филтриране по статус на завършеност
(showCompleted || !task.isCompleted)
}
}
// Използване на предиката
let predicate = buildTaskPredicate(
searchText: "пазаруване",
showCompleted: false,
minimumPriority: .medium
)
var descriptor = FetchDescriptor(predicate: predicate)
descriptor.sortBy = [
SortDescriptor(\Task.createdAt, order: .reverse)
]
let filteredTasks = try context.fetch(descriptor)
Динамични заявки с @Query
В SwiftUI изгледите можете да направите @Query динамичен чрез инициализатора на изгледа. Този подход е полезен, когато параметрите на заявката зависят от потребителски вход:
struct FilteredTasksView: View {
@Query private var tasks: [Task]
init(showCompleted: Bool = false, searchText: String = "") {
let predicate = #Predicate { task in
showCompleted || !task.isCompleted
}
_tasks = Query(
filter: predicate,
sort: \Task.createdAt,
order: .reverse
)
}
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
}
}
Броене на резултати и агрегиране
// Броене без зареждане на обекти в паметта
func countPendingTasks(context: ModelContext) throws -> Int {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.isCompleted }
)
return try context.fetchCount(descriptor)
}
// Проверка дали съществуват резултати
func hasOverdueTasks(context: ModelContext, deadline: Date) throws -> Bool {
var descriptor = FetchDescriptor(
predicate: #Predicate { task in
!task.isCompleted && task.createdAt < deadline
}
)
descriptor.fetchLimit = 1
let results = try context.fetch(descriptor)
return !results.isEmpty
}
Релации между модели
Релациите в SwiftData се дефинират чрез макрото @Relationship и обикновени Swift свойства. SwiftData поддържа един-към-много и много-към-много релации, както и инверсни релации, които автоматично се синхронизират. Нека видим как работи на практика.
Един-към-много релация
@Model
final class Author {
var name: String
var bio: String
// Един автор може да има много книги
// При изтриване на автора — всички негови книги също се изтриват
@Relationship(deleteRule: .cascade, inverse: \Book.author)
var books: [Book]
init(name: String, bio: String = "") {
self.name = name
self.bio = bio
self.books = []
}
}
@Model
final class Book {
var title: String
var publishedDate: Date
var isbn: String
// Всяка книга принадлежи на един автор
var author: Author?
init(title: String, isbn: String, publishedDate: Date = Date()) {
self.title = title
self.isbn = isbn
self.publishedDate = publishedDate
}
}
Параметърът deleteRule определя какво се случва с релацираните обекти при изтриване. Ето опциите:
.cascade— изтрива и всички свързани обекти (подходящо за силни зависимости).nullify— задава релацията на nil, без да изтрива свързаните обекти (по подразбиране).deny— предотвратява изтриването, ако има свързани обекти.noAction— не прави нищо (внимавайте с тази — може да остави осиротели обекти)
Много-към-много релация
@Model
final class Student {
var name: String
var studentId: String
// Всеки студент може да посещава много курсове
@Relationship(inverse: \Course.students)
var courses: [Course]
init(name: String, studentId: String) {
self.name = name
self.studentId = studentId
self.courses = []
}
}
@Model
final class Course {
var title: String
var code: String
var credits: Int
// Всеки курс може да има много студенти
var students: [Student]
init(title: String, code: String, credits: Int) {
self.title = title
self.code = code
self.credits = credits
self.students = []
}
}
// Работа с много-към-много релация
func enrollStudent(student: Student, in course: Course) {
student.courses.append(course)
// Инверсната релация автоматично се обновява
// course.students вече съдържа student
}
Самореференчиращи се релации
SwiftData поддържа и самореференчиращи се релации — изключително полезни за йерархични структури като категории или организационни диаграми:
@Model
final class Category {
var name: String
// Родителска категория
var parent: Category?
// Дъщерни категории
@Relationship(deleteRule: .cascade, inverse: \Category.parent)
var children: [Category]
init(name: String, parent: Category? = nil) {
self.name = name
self.parent = parent
self.children = []
}
// Изчисляемо свойство за пълния път
var fullPath: String {
if let parent {
return "\(parent.fullPath) > \(name)"
}
return name
}
}
Миграция на схемата
В реалния свят моделът на данни не остава статичен. Добавяте нови свойства, преименувате полета, реорганизирате релации. Това е неизбежно. SwiftData предоставя структуриран подход за управление на тези промени чрез VersionedSchema и SchemaMigrationPlan.
Дефиниране на версии на схемата
// Версия 1 — оригиналният модел
enum TaskSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
final class Task {
var title: String
var isCompleted: Bool
init(title: String) {
self.title = title
self.isCompleted = false
}
}
}
// Версия 2 — добавяне на приоритет
enum TaskSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
final class Task {
var title: String
var isCompleted: Bool
var priority: Int
init(title: String, priority: Int = 0) {
self.title = title
self.isCompleted = false
self.priority = priority
}
}
}
// Версия 3 — добавяне на дата на краен срок и бележки
enum TaskSchemaV3: VersionedSchema {
static var versionIdentifier = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
final class Task {
var title: String
var isCompleted: Bool
var priority: Int
var dueDate: Date?
var notes: String
init(title: String, priority: Int = 0) {
self.title = title
self.isCompleted = false
self.priority = priority
self.notes = ""
}
}
}
Дефиниране на план за миграция
enum TaskMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TaskSchemaV1.self, TaskSchemaV2.self, TaskSchemaV3.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
// Лека миграция — достатъчна когато само добавяте нови свойства
// с подразбиращи се стойности
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: TaskSchemaV1.self,
toVersion: TaskSchemaV2.self
)
// Персонализирана миграция — когато трябва да трансформирате данни
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: TaskSchemaV2.self,
toVersion: TaskSchemaV3.self,
willMigrate: { context in
// Предварителна обработка преди миграция, ако е необходимо
},
didMigrate: { context in
// След миграцията — задаване на стойности по подразбиране
let tasks = try context.fetch(FetchDescriptor())
for task in tasks {
if task.priority > 1 {
// Задачи с висок приоритет получават краен срок след 7 дни
task.dueDate = Calendar.current.date(
byAdding: .day, value: 7, to: Date()
)
}
}
try context.save()
}
)
}
Прилагане на плана за миграция
@main
struct MyApp: App {
let container: ModelContainer
init() {
do {
container = try ModelContainer(
for: TaskSchemaV3.Task.self,
migrationPlan: TaskMigrationPlan.self
)
} catch {
fatalError("Неуспешна миграция: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Леката миграция (.lightweight) е подходяща за прости промени — добавяне на нови свойства с подразбиращи се стойности, преименуване (с @Attribute(originalName:)) и добавяне или премахване на индекси. Персонализираната миграция (.custom) влиза в действие когато трябва да трансформирате данни — например да разделите едно поле на две, да обедините записи или да изчислите нови стойности. В моя опит повечето миграции могат да минат с леката версия, но е добре да познавате и двата варианта.
Ново в iOS 26: Наследяване на модели
Тук е може би най-вълнуващата част. Една от най-дългоочакваните функции идва с iOS 26 — поддръжка на наследяване на класове с @Model. Преди iOS 26, всеки @Model клас трябваше да бъде final и нямаше начин да наследява друг @Model клас. Това беше сериозно ограничение, което принуждаваше разработчиците да прибягват до композиция или протоколи за случаи, в които наследяването е естественият избор.
Базов клас и подкласове
// Базов клас за събитие — вече не е задължително да бъде final
@Model
class Event {
var title: String
var date: Date
var location: String
init(title: String, date: Date, location: String = "") {
self.title = title
self.date = date
self.location = location
}
}
// Социално събитие с допълнителни свойства
@available(iOS 26, *)
@Model
final class SocialEvent: Event {
var attendees: [String]
var dressCode: String
init(title: String, date: Date, attendees: [String], dressCode: String = "Casual") {
self.attendees = attendees
self.dressCode = dressCode
super.init(title: title, date: date)
}
}
// Работно събитие
@available(iOS 26, *)
@Model
final class WorkEvent: Event {
var organizer: String
var isRequired: Bool
var meetingLink: String?
init(title: String, date: Date, organizer: String, isRequired: Bool = false) {
self.organizer = organizer
self.isRequired = isRequired
super.init(title: title, date: date)
}
}
// Спортно събитие
@available(iOS 26, *)
@Model
final class SportEvent: Event {
var sport: String
var teams: [String]
var venue: String
init(title: String, date: Date, sport: String, teams: [String], venue: String) {
self.sport = sport
self.teams = teams
self.venue = venue
super.init(title: title, date: date, location: venue)
}
}
Полиморфни заявки
Едно от най-мощните предимства на наследяването е възможността да правите заявки към базовия клас и да получите всички подтипове наведнъж:
@available(iOS 26, *)
struct EventCalendarView: View {
// Заявка към базовия клас — връща ВСИЧКИ видове събития
@Query(sort: \Event.date) private var allEvents: [Event]
// Заявка само към работни събития
@Query(
filter: #Predicate { $0.isRequired },
sort: \WorkEvent.date
)
private var requiredMeetings: [WorkEvent]
var body: some View {
List {
Section("Всички събития") {
ForEach(allEvents) { event in
EventRow(event: event)
}
}
Section("Задължителни срещи") {
ForEach(requiredMeetings) { meeting in
WorkEventRow(event: meeting)
}
}
}
}
}
@available(iOS 26, *)
struct EventRow: View {
let event: Event
var body: some View {
VStack(alignment: .leading) {
Text(event.title)
.font(.headline)
Text(event.date, style: .date)
.font(.subheadline)
// Проверка на типа за специфично визуализиране
if let social = event as? SocialEvent {
Text("\(social.attendees.count) участници")
.font(.caption)
.foregroundStyle(.blue)
} else if let work = event as? WorkEvent {
Label(
work.isRequired ? "Задължително" : "Незадължително",
systemImage: work.isRequired ? "exclamationmark.circle" : "info.circle"
)
.font(.caption)
} else if let sport = event as? SportEvent {
Text(sport.sport)
.font(.caption)
.foregroundStyle(.green)
}
}
}
}
Практически ползи от наследяването
Наследяването на модели решава няколко важни архитектурни проблема:
- Полиморфизъм — работите с хетерогенни колекции от обекти чрез общ базов тип, без да жонглирате с enum-и или протоколни абстракции
- Споделена логика — общите свойства и методи живеят в базовия клас, без дублиране
- Едно хранилище — всички подтипове се съхраняват в една таблица с дискриминатор колона, което опростява заявките
- Гъвкавост при заявки — извличате всички типове през базовия клас или само конкретен подтип
// Регистриране в ModelContainer — базовият клас е достатъчен
@available(iOS 26, *)
let container = try ModelContainer(
for: Event.self // Автоматично включва всички подкласове
)
// Програмно извличане на всички социални събития тази седмица
@available(iOS 26, *)
func fetchThisWeekSocialEvents(context: ModelContext) throws -> [SocialEvent] {
let now = Date()
let endOfWeek = Calendar.current.date(byAdding: .day, value: 7, to: now)!
let descriptor = FetchDescriptor(
predicate: #Predicate { event in
event.date >= now && event.date <= endOfWeek
},
sortBy: [SortDescriptor(\.date)]
)
return try context.fetch(descriptor)
}
Имайте предвид, че наследяването в iOS 26 има някои ограничения. Не е добра идея йерархията да бъде прекалено дълбока — препоръчително е да се придържате към едно ниво (базов клас и директни подкласове). Промените в йерархията на наследяване също изискват внимателна миграция на схемата.
Оптимизация на производителността
SwiftData е проектиран да бъде ефективен по подразбиране, но в продукционни приложения с големи обеми данни се налага да обърнете допълнително внимание на производителността. Ето ключовите техники.
Ограничаване на извличаните данни
// Извличане само на нужните свойства
func fetchTaskTitles(context: ModelContext) throws -> [Task] {
var descriptor = FetchDescriptor()
// Зареждане само на заглавието и статуса — не зарежда подзадачи,
// бележки и други тежки свойства
descriptor.propertiesToFetch = [\.title, \.isCompleted]
// Ограничаване на броя резултати
descriptor.fetchLimit = 50
// Пропускане на първите 20 резултата (пагинация)
descriptor.fetchOffset = 20
return try context.fetch(descriptor)
}
// Пагинация с пълна реализация
struct PaginatedTasksView: View {
@State private var tasks: [Task] = []
@State private var currentPage = 0
@Environment(\.modelContext) private var context
private let pageSize = 25
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
// Зареждане на следващата страница при достигане на края
Button("Зареди още") {
loadNextPage()
}
}
.onAppear { loadNextPage() }
}
private func loadNextPage() {
var descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
descriptor.fetchLimit = pageSize
descriptor.fetchOffset = currentPage * pageSize
if let newTasks = try? context.fetch(descriptor) {
tasks.append(contentsOf: newTasks)
currentPage += 1
}
}
}
Фонови контексти
За тежки операции — като импортиране на големи масиви от данни или сложни трансформации — използвайте фонови контексти. Главната нишка ви благодари:
actor DataImporter {
let container: ModelContainer
init(container: ModelContainer) {
self.container = container
}
// Импортиране на данни във фонов контекст
func importTasks(from data: [TaskDTO]) async throws {
let context = ModelContext(container)
// Деактивиране на автоматичния запис за по-добра производителност
// при групови операции
context.autosaveEnabled = false
for (index, dto) in data.enumerated() {
let task = Task(title: dto.title)
task.isCompleted = dto.isCompleted
context.insert(task)
// Периодичен запис на всеки 100 обекта
// за управление на паметта
if index % 100 == 0 {
try context.save()
}
}
// Финален запис
try context.save()
}
}
// Използване в SwiftUI
struct ImportView: View {
@Environment(\.modelContext) private var context
@State private var isImporting = false
var body: some View {
Button("Импортирай данни") {
isImporting = true
Task {
let importer = DataImporter(
container: context.container
)
do {
try await importer.importTasks(from: sampleData)
isImporting = false
} catch {
print("Грешка при импортиране: \(error)")
isImporting = false
}
}
}
.disabled(isImporting)
}
}
Ефективно използване на @Query
// Избягвайте заявки, които зареждат прекалено много данни
// ЛОШО — зарежда всички задачи, дори ако показвате само 10
@Query private var allTasks: [Task]
// ДОБРЕ — ограничете броя резултати
@Query(
sort: \Task.createdAt,
order: .reverse
) private var recentTasks: [Task]
// ОЩЕ ПО-ДОБРЕ — филтрирайте на ниво заявка
@Query(
filter: #Predicate { !$0.isCompleted },
sort: \Task.createdAt,
order: .reverse
) private var pendingTasks: [Task]
Индексиране за по-бързи заявки
@Model
final class Article {
// Индексиране на свойства, по които често филтрирате или сортирате
@Attribute(.unique) var slug: String
var title: String
var content: String
var publishedAt: Date
var isPublished: Bool
var category: String
var viewCount: Int
init(slug: String, title: String, content: String) {
self.slug = slug
self.title = title
self.content = content
self.publishedAt = Date()
self.isPublished = false
self.category = ""
self.viewCount = 0
}
}
// Индексите се дефинират в Schema — подобрява бързината на заявки
// по publishedAt и category
extension Article {
static let schemaMetadata: Schema = Schema(
[Article.self],
version: Schema.Version(1, 0, 0)
)
}
Най-добри практики за продукция
Изграждането на стабилен слой за данни изисква повече от познаване на API-то. Ето практики, които съм намерил за полезни в реални продукционни приложения.
Обработка на грешки
SwiftData операциите могат да хвърлят грешки и е критично важно да ги обработвате правилно. Не подценявайте тази стъпка:
enum DataError: LocalizedError {
case saveFailed(underlying: Error)
case fetchFailed(underlying: Error)
case migrationFailed(underlying: Error)
case containerCreationFailed(underlying: Error)
var errorDescription: String? {
switch self {
case .saveFailed(let error):
return "Неуспешен запис на данни: \(error.localizedDescription)"
case .fetchFailed(let error):
return "Неуспешно извличане на данни: \(error.localizedDescription)"
case .migrationFailed(let error):
return "Неуспешна миграция: \(error.localizedDescription)"
case .containerCreationFailed(let error):
return "Неуспешно създаване на хранилище: \(error.localizedDescription)"
}
}
}
// Централизиран слой за работа с данни
@Observable
final class DataService {
private let container: ModelContainer
var lastError: DataError?
init() throws {
do {
container = try ModelContainer(for: Task.self, Subtask.self)
} catch {
throw DataError.containerCreationFailed(underlying: error)
}
}
var mainContext: ModelContext {
container.mainContext
}
func save() throws {
do {
try mainContext.save()
} catch {
let dataError = DataError.saveFailed(underlying: error)
lastError = dataError
throw dataError
}
}
func fetch(
_ descriptor: FetchDescriptor
) throws -> [T] {
do {
return try mainContext.fetch(descriptor)
} catch {
let dataError = DataError.fetchFailed(underlying: error)
lastError = dataError
throw dataError
}
}
}
Архитектурни шаблони
За по-големи приложения е добра практика да изолирате слоя за данни зад абстракция. Това улеснява тестването и позволява смяна на имплементацията без промени в бизнес логиката. Звучи като допълнителна работа, но вярвайте ми — оправдава се:
// Протокол за хранилище — дефинира интерфейса за работа с данни
protocol TaskRepository {
func fetchAll() async throws -> [Task]
func fetchPending() async throws -> [Task]
func create(title: String) async throws -> Task
func update(_ task: Task) async throws
func delete(_ task: Task) async throws
func deleteCompleted() async throws
}
// Конкретна имплементация с SwiftData
final class SwiftDataTaskRepository: TaskRepository {
private let context: ModelContext
init(context: ModelContext) {
self.context = context
}
func fetchAll() async throws -> [Task] {
let descriptor = FetchDescriptor(
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return try context.fetch(descriptor)
}
func fetchPending() async throws -> [Task] {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.isCompleted },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return try context.fetch(descriptor)
}
func create(title: String) async throws -> Task {
let task = Task(title: title)
context.insert(task)
try context.save()
return task
}
func update(_ task: Task) async throws {
// Промените се проследяват автоматично
try context.save()
}
func delete(_ task: Task) async throws {
context.delete(task)
try context.save()
}
func deleteCompleted() async throws {
try context.delete(
model: Task.self,
where: #Predicate { $0.isCompleted }
)
try context.save()
}
}
// Мок имплементация за тестове
final class MockTaskRepository: TaskRepository {
var tasks: [Task] = []
var shouldThrow = false
func fetchAll() async throws -> [Task] {
if shouldThrow { throw DataError.fetchFailed(underlying: NSError()) }
return tasks
}
func fetchPending() async throws -> [Task] {
return tasks.filter { !$0.isCompleted }
}
func create(title: String) async throws -> Task {
let task = Task(title: title)
tasks.append(task)
return task
}
func update(_ task: Task) async throws {}
func delete(_ task: Task) async throws {
tasks.removeAll { $0.id == task.id }
}
func deleteCompleted() async throws {
tasks.removeAll { $0.isCompleted }
}
}
Тестване на SwiftData
За unit тестове използвайте in-memory контейнер — гарантира изолирани и бързи тестове:
import Testing
import SwiftData
@Suite("Тестове за Task модела")
struct TaskModelTests {
let container: ModelContainer
let context: ModelContext
init() throws {
// In-memory контейнер за тестове — няма запис на диска
let config = ModelConfiguration(isStoredInMemoryOnly: true)
container = try ModelContainer(
for: Task.self, Subtask.self,
configurations: config
)
context = container.mainContext
}
@Test("Създаване на задача с правилни начални стойности")
func createTask() throws {
let task = Task(title: "Тестова задача")
context.insert(task)
try context.save()
let descriptor = FetchDescriptor()
let tasks = try context.fetch(descriptor)
#expect(tasks.count == 1)
#expect(tasks.first?.title == "Тестова задача")
#expect(tasks.first?.isCompleted == false)
}
@Test("Изтриване на завършени задачи")
func deleteCompletedTasks() throws {
// Подготовка
let task1 = Task(title: "Завършена")
task1.isCompleted = true
let task2 = Task(title: "Незавършена")
context.insert(task1)
context.insert(task2)
try context.save()
// Действие
try context.delete(
model: Task.self,
where: #Predicate { $0.isCompleted }
)
try context.save()
// Проверка
let remaining = try context.fetch(FetchDescriptor())
#expect(remaining.count == 1)
#expect(remaining.first?.title == "Незавършена")
}
@Test("Каскадно изтриване на подзадачи")
func cascadeDeleteSubtasks() throws {
let task = Task(title: "Главна задача")
let subtask1 = Subtask(title: "Подзадача 1")
let subtask2 = Subtask(title: "Подзадача 2")
task.subtasks = [subtask1, subtask2]
context.insert(task)
try context.save()
// Изтриване на главната задача
context.delete(task)
try context.save()
// Подзадачите трябва да са изтрити заради каскадното правило
let subtasks = try context.fetch(FetchDescriptor())
#expect(subtasks.isEmpty)
}
@Test("Търсене с предикат")
func searchWithPredicate() throws {
let task1 = Task(title: "Купи хляб")
let task2 = Task(title: "Купи мляко")
let task3 = Task(title: "Пиши код")
context.insert(task1)
context.insert(task2)
context.insert(task3)
try context.save()
let searchTerm = "Купи"
let descriptor = FetchDescriptor(
predicate: #Predicate { task in
task.title.localizedStandardContains(searchTerm)
}
)
let results = try context.fetch(descriptor)
#expect(results.count == 2)
}
}
Обработка на конкурентен достъп
Когато работите с множество контексти (например главен и фонов), трябва да внимавате за конкурентния достъп. Тук @ModelActor е вашият най-добър приятел:
// Безопасна работа с фонов контекст чрез ModelActor
@ModelActor
actor BackgroundProcessor {
// Групово обновяване на статус
func markAllAsCompleted() throws {
let descriptor = FetchDescriptor(
predicate: #Predicate { !$0.isCompleted }
)
let tasks = try modelContext.fetch(descriptor)
for task in tasks {
task.isCompleted = true
}
try modelContext.save()
}
// Синхронизиране на данни от сървър
func syncFromServer(data: [TaskDTO]) throws {
modelContext.autosaveEnabled = false
for dto in data {
// Проверка дали задачата вече съществува
let existingDescriptor = FetchDescriptor(
predicate: #Predicate { task in
task.title == dto.title
}
)
let existing = try modelContext.fetch(existingDescriptor)
if let existingTask = existing.first {
// Обновяване на съществуваща задача
existingTask.isCompleted = dto.isCompleted
} else {
// Създаване на нова задача
let newTask = Task(title: dto.title)
newTask.isCompleted = dto.isCompleted
modelContext.insert(newTask)
}
}
try modelContext.save()
}
}
// Използване на ModelActor
struct SyncView: View {
@Environment(\.modelContext) private var context
var body: some View {
Button("Синхронизирай") {
Task {
let processor = BackgroundProcessor(
modelContainer: context.container
)
try await processor.syncFromServer(data: serverData)
}
}
}
}
Работа с CloudKit
SwiftData поддържа синхронизация с CloudKit с минимална конфигурация. Буквално няколко реда код:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Task.self, isAutosaveEnabled: true)
// CloudKit се активира автоматично ако проектът има
// CloudKit entitlement и iCloud контейнер
}
}
// За ръчна конфигурация с CloudKit
let container = try ModelContainer(
for: Task.self,
configurations: ModelConfiguration(
cloudKitDatabase: .automatic
)
)
Практически съвети за продукция
- Винаги тествайте миграциите — създайте unit тестове, които симулират миграция от всяка предишна версия. Грешка при миграция в продукция означава загуба на потребителски данни. А това не искате.
- Използвайте
.uniqueатрибути — те предотвратяват дублирани записи и поддържат upsert семантика, което е идеално за синхронизация с API. - Не зареждайте всичко наведнъж — използвайте
fetchLimitиfetchOffsetза пагинация. За списъци с хиляди елементи това е критично. - Групови операции извършвайте във фонов контекст —
@ModelActorе създаден точно за това. - Деактивирайте autosave за групови операции — задайте
context.autosaveEnabled = falseи извикайтеsave()ръчно на определени интервали. - Обработвайте грешки навсякъде — всяка операция с контекста може да хвърли грешка. Не използвайте
try!в продукционен код. Никога. - Изолирайте слоя за данни — Repository шаблонът отделя бизнес логиката от имплементацията на хранилището.
- Проектирайте за бъдещи миграции — винаги добавяйте нови свойства с подразбиращи се стойности. Планирайте версионирането на схемата от самото начало.
Чести грешки, които да избягвате
Позволете ми да спомена и няколко грешки, които виждам често (и самият аз съм допускал):
// ГРЕШКА 1: Използване на контекста в грешна нишка
// ModelContext НЕ е thread-safe — използвайте го само от нишката,
// на която е създаден
// ГРЕШКА — достъп до главния контекст от фонова нишка
Task.detached {
let context = container.mainContext // Опасно!
// ... операции
}
// ПРАВИЛНО — създайте нов контекст за фоновата нишка
Task.detached {
let context = ModelContext(container) // Безопасно
// ... операции
}
// ГРЕШКА 2: Предаване на модели между контексти
// Модел, извлечен от един контекст, НЕ може да се използва в друг
// ГРЕШКА
let task = try mainContext.fetch(descriptor).first!
let backgroundContext = ModelContext(container)
backgroundContext.delete(task) // Може да доведе до срив
// ПРАВИЛНО — извлечете отново от правилния контекст
let taskId = task.persistentModelID
let backgroundContext = ModelContext(container)
if let backgroundTask = backgroundContext.model(for: taskId) as? Task {
backgroundContext.delete(backgroundTask) // Безопасно
}
// ГРЕШКА 3: Забравяне на try при save()
context.insert(newTask)
// context.save() // Компилаторът ще ви предупреди, но не пропускайте обработката
try context.save() // Винаги обработвайте грешките
Заключение
SwiftData наистина представлява фундаментална промяна в начина, по който управляваме данни в Swift приложенията. От декларативното дефиниране на модели с @Model, през типово безопасните заявки с #Predicate, до структурираната миграция с VersionedSchema — всичко е проектирано с мисълта за продуктивността на разработчика.
С iOS 26 и поддръжката на наследяване на класове, SwiftData затваря една от последните празнини спрямо Core Data и отваря вратата за по-елегантни архитектурни решения. Полиморфните заявки и споделената логика в базовите класове правят моделирането на сложни домейни значително по-естествено.
Ключът към успешното използване на SwiftData в продукция? Комбинацията от правилна архитектура (Repository шаблон), внимателна обработка на грешки, планирана миграция на схемата и оптимизация чрез ограничаване на заявките и фонови контексти. Следвайки практиките от това ръководство, ще изградите слой за данни, който ще ви служи надеждно.
SwiftData не е просто по-нов Core Data. Той е модерен, Swift-native фреймуърк, който уважава парадигмите на езика и екосистемата. Ако тепърва започвате нов проект или планирате миграция от Core Data — сега е моментът.