SwiftData: полное руководство — от моделей данных до синхронизации с CloudKit

Разбираем SwiftData от А до Я: создание моделей с @Model, CRUD-операции, @Query с фильтрацией, связи между моделями, миграции схемы, наследование в iOS 26 и настройка CloudKit-синхронизации. С рабочими примерами кода.

SwiftData 2026: @Model, CloudKit и миграции

Введение: что такое SwiftData и зачем он вообще нужен

SwiftData — это современный фреймворк Apple для локального хранения данных, впервые показанный на WWDC 2023 и доступный начиная с iOS 17. Если вы работали с Core Data, то знаете, сколько боли приносит этот фреймворк — громоздкие .xcdatamodeld-файлы, NSManagedObject-подклассы, обязательная настройка стека. SwiftData — это, по сути, Core Data, но переосмысленный для эпохи Swift.

Меньше шаблонного кода, декларативный подход, нативная интеграция со SwiftUI. И при этом под капотом всё тот же проверенный движок Core Data.

В этом руководстве мы пройдём весь путь: от создания первой модели до настройки синхронизации с iCloud через CloudKit. Будут рабочие примеры кода, практические советы и разбор подводных камней, которые вы вряд ли найдёте в официальной документации.

Настройка проекта: ModelContainer и ModelContext

Итак, работа со SwiftData строится вокруг двух ключевых объектов: ModelContainer и ModelContext.

Контейнер (ModelContainer) — это хранилище, которое управляет схемой данных и связью с физической базой (по умолчанию SQLite). Контекст (ModelContext) — это своего рода «черновик», через который вы работаете с данными: вставляете, обновляете, удаляете. Если вы знакомы с Core Data, то это прямые аналоги NSPersistentContainer и NSManagedObjectContext.

Подключение на уровне приложения

Самый простой способ настроить SwiftData — добавить модификатор .modelContainer(for:) к корневой сцене:

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [Task.self, Category.self])
        }
    }
}

Этот модификатор создаёт контейнер для указанных моделей и внедряет ModelContext в окружение SwiftUI. Все дочерние view автоматически получают доступ к контексту через @Environment(\.modelContext). Честно говоря, по сравнению с настройкой стека Core Data — это просто праздник.

Расширенная настройка контейнера

Если нужна более тонкая настройка, используйте ModelConfiguration:

let config = ModelConfiguration(
    "MyAppStore",
    schema: Schema([Task.self, Category.self]),
    isStoredInMemoryOnly: false,
    cloudKitDatabase: .automatic
)

let container = try ModelContainer(
    for: Task.self, Category.self,
    configurations: config
)

Параметр isStoredInMemoryOnly: true пригодится для тестирования и превью в SwiftUI — данные живут только в оперативной памяти и не записываются на диск. Очень удобно для юнит-тестов.

Определение моделей с макросом @Model

Помните, как в Core Data модели определялись через визуальный редактор .xcdatamodeld и громоздкие NSManagedObject-подклассы? SwiftData радикально упрощает этот процесс. Достаточно добавить макрос @Model к обычному Swift-классу:

import SwiftData

@Model
class Task {
    var title: String
    var notes: String
    var isCompleted: Bool
    var createdAt: Date
    var priority: Int

    init(title: String, notes: String = "", isCompleted: Bool = false, priority: Int = 0) {
        self.title = title
        self.notes = notes
        self.isCompleted = isCompleted
        self.createdAt = Date()
        self.priority = priority
    }
}

Макрос @Model автоматически генерирует всю инфраструктуру для сохранения, загрузки и отслеживания изменений. Поддерживаются стандартные типы: String, Int, Double, Bool, Date, Data, URL, UUID, а также перечисления, соответствующие Codable.

Атрибуты свойств

Для дополнительной настройки поведения свойств есть макрос @Attribute:

@Model
class User {
    @Attribute(.unique) var email: String      // уникальное значение
    @Attribute(.externalStorage) var avatar: Data? // хранение в отдельном файле
    @Attribute(.spotlight) var name: String      // индексация для Spotlight
    @Transient var temporaryScore: Int = 0       // не сохраняется в базу

    init(email: String, name: String, avatar: Data? = nil) {
        self.email = email
        self.name = name
        self.avatar = avatar
    }
}

Атрибут .unique гарантирует уникальность значения — при попытке вставить дубликат SwiftData выполнит upsert (обновление существующей записи вместо создания новой). Атрибут .externalStorage рекомендуется для больших бинарных данных вроде изображений — они хранятся отдельно от основной базы SQLite, что заметно улучшает производительность.

CRUD-операции: создание, чтение, обновление, удаление

Давайте разберём все четыре базовые операции на примере приложения для управления задачами.

Создание (Create)

Для добавления новой записи используется метод insert(_:) контекста:

struct AddTaskView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var title = ""

    var body: some View {
        Form {
            TextField("Название задачи", text: $title)
            Button("Добавить") {
                let task = Task(title: title)
                modelContext.insert(task)
                // SwiftData автоматически сохранит изменения
            }
        }
    }
}

По умолчанию SwiftData включает автосохранение — изменения записываются в базу при определённых событиях (уход приложения в фон, изменения UI и т.д.). Если нужно сохранить принудительно — вызывайте try modelContext.save().

Чтение (Read)

Для выборки данных в SwiftUI есть обёртка свойства @Query:

struct TaskListView: View {
    @Query(sort: \Task.createdAt, order: .reverse)
    private var tasks: [Task]

    var body: some View {
        List(tasks) { task in
            HStack {
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                VStack(alignment: .leading) {
                    Text(task.title)
                    Text(task.createdAt, style: .date)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
        }
    }
}

@Query автоматически обновляет view при любых изменениях данных. Удалили задачу в другом экране — список тут же перерисуется. Никаких делегатов и NSFetchedResultsController.

Обновление (Update)

А вот обновление в SwiftData — это, пожалуй, самое приятное. Просто меняете свойство объекта, и всё:

struct TaskDetailView: View {
    @Bindable var task: Task

    var body: some View {
        Form {
            TextField("Название", text: $task.title)
            Toggle("Выполнена", isOn: $task.isCompleted)
            Stepper("Приоритет: \(task.priority)", value: $task.priority, in: 0...5)
        }
    }
}

Поскольку модели SwiftData — это классы (ссылочные типы), любое изменение свойства напрямую обновляет объект в контексте. SwiftData сам отслеживает все изменения и сохраняет их. Никакого явного вызова save не нужно.

Удаление (Delete)

Для удаления записи — метод delete(_:) контекста:

struct TaskListView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var tasks: [Task]

    var body: some View {
        List {
            ForEach(tasks) { task in
                TaskRowView(task: task)
            }
            .onDelete { indexSet in
                for index in indexSet {
                    modelContext.delete(tasks[index])
                }
            }
        }
    }
}

@Query: фильтрация, сортировка и динамические запросы

Макрос @Query — это гораздо больше, чем просто «показать всё из базы». Он поддерживает фильтрацию через предикаты, сортировку и ограничение количества результатов.

Фильтрация с #Predicate

Для фильтрации данных используется макрос #Predicate, который принимает обычное Swift-выражение:

// Только невыполненные задачи
@Query(filter: #Predicate<Task> { !$0.isCompleted })
private var pendingTasks: [Task]

// Задачи с высоким приоритетом
@Query(filter: #Predicate<Task> { $0.priority >= 4 })
private var highPriorityTasks: [Task]

// Поиск по названию
@Query(filter: #Predicate<Task> { task in
    task.title.localizedStandardContains("покупки")
})
private var shoppingTasks: [Task]

Важный момент: фильтрация с #Predicate выполняется на уровне базы данных, а не в памяти. Это значительно эффективнее, чем загрузить все записи и потом фильтровать через .filter() в Swift. На больших объёмах данных разница будет ощутимой.

Сортировка с SortDescriptor

Для многоуровневой сортировки используйте массив SortDescriptor:

// Сортировка по приоритету (убывание), затем по названию
@Query(sort: [
    SortDescriptor(\Task.priority, order: .reverse),
    SortDescriptor(\Task.title)
])
private var sortedTasks: [Task]

Совет из практики: всегда добавляйте хотя бы два критерия сортировки — основной и запасной. Иначе порядок записей с одинаковым значением первого критерия будет непредсказуемым, и UI может «прыгать».

Динамические запросы

У @Query есть одно серьёзное ограничение: параметры фильтрации и сортировки задаются при объявлении свойства и не могут меняться на лету. Для динамических запросов используется паттерн с вынесением @Query в отдельное дочернее view:

// Родительское view с элементами управления
struct TasksScreen: View {
    @State private var searchText = ""
    @State private var showCompleted = false
    @State private var sortOrder = SortDescriptor(\Task.createdAt, order: .reverse)

    var body: some View {
        FilteredTaskList(
            searchText: searchText,
            showCompleted: showCompleted,
            sortOrder: sortOrder
        )
    }
}

// Дочернее view с @Query
struct FilteredTaskList: View {
    @Query private var tasks: [Task]

    init(searchText: String, showCompleted: Bool, sortOrder: SortDescriptor<Task>) {
        let predicate = #Predicate<Task> { task in
            (searchText.isEmpty || task.title.localizedStandardContains(searchText)) &&
            (showCompleted || !task.isCompleted)
        }
        _tasks = Query(filter: predicate, sort: [sortOrder])
    }

    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
    }
}

Суть простая: при каждом изменении параметров SwiftUI пересоздаёт дочернее view с новыми аргументами для @Query, и запрос выполняется заново. Не самый элегантный паттерн (было бы здорово, если бы Apple сделали @Query динамическим из коробки), но работает надёжно.

FetchDescriptor для продвинутых сценариев

Когда нужен полный контроль — ограничение количества записей, смещение, пакетная обработка — на помощь приходит FetchDescriptor:

// Последние 10 задач
var descriptor = FetchDescriptor<Task>(
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
descriptor.fetchLimit = 10

let recentTasks = try modelContext.fetch(descriptor)

// Подсчёт записей без загрузки данных
let count = try modelContext.fetchCount(
    FetchDescriptor<Task>(predicate: #Predicate { !$0.isCompleted })
)

Связи между моделями

Реальные приложения редко обходятся одной моделью — данные почти всегда связаны между собой. SwiftData поддерживает связи «один к одному», «один ко многим» и «многие ко многим».

Один ко многим (one-to-many)

Это самый распространённый тип связи. Например, у одной категории может быть много задач:

@Model
class Category {
    var name: String
    var color: String

    @Relationship(deleteRule: .cascade, inverse: \Task.category)
    var tasks: [Task]

    init(name: String, color: String = "blue") {
        self.name = name
        self.color = color
        self.tasks = []
    }
}

@Model
class Task {
    var title: String
    var isCompleted: Bool
    var category: Category?

    init(title: String, isCompleted: Bool = false, category: Category? = nil) {
        self.title = title
        self.isCompleted = isCompleted
        self.category = category
    }
}

Обратите внимание на deleteRule: .cascade — при удалении категории все её задачи тоже удалятся. Если хотите сохранить задачи при удалении категории, используйте .nullify (это поведение по умолчанию) — тогда свойство category у задач просто станет nil.

Правила удаления

SwiftData поддерживает три правила удаления:

  • .nullify (по умолчанию) — связанные объекты сохраняются, ссылка обнуляется
  • .cascade — связанные объекты удаляются вместе с родителем
  • .noAction — ничего не происходит (будьте осторожны: можно получить «осиротевшие» записи в базе)

Есть один неочевидный момент: каскадное удаление корректно работает только при включённом автосохранении. Если вы отключили его (autosaveEnabled: false), каскадное удаление может не сработать. Это известная проблема SwiftData, и на неё стоит обратить внимание, если вы управляете сохранением вручную.

Многие ко многим (many-to-many)

Для связи «многие ко многим» всё ещё проще — используйте массивы с обеих сторон:

@Model
class Student {
    var name: String
    var courses: [Course]

    init(name: String, courses: [Course] = []) {
        self.name = name
        self.courses = courses
    }
}

@Model
class Course {
    var title: String
    var students: [Student]

    init(title: String, students: [Student] = []) {
        self.title = title
        self.students = students
    }
}

SwiftData сам создаёт промежуточную таблицу для хранения связи — вам не нужно об этом думать. Приятно, правда?

Миграция схемы данных

По мере развития приложения модель данных неизбежно меняется. SwiftData предлагает два уровня миграции: лёгкую (автоматическую) и ручную с использованием версий схемы.

Лёгкая миграция

SwiftData автоматически справляется с простыми изменениями:

  • Добавление новых свойств с значениями по умолчанию
  • Удаление свойств
  • Переименование свойств (через @Attribute(originalName:))
@Model
class Task {
    var title: String
    var isCompleted: Bool

    // Переименовано свойство — SwiftData знает, откуда брать данные
    @Attribute(originalName: "desc")
    var notes: String

    // Новое свойство с значением по умолчанию — безопасно добавлять
    var priority: Int = 0
}

Версионированная миграция

Для более сложных изменений — преобразования типов, объединения полей — понадобятся VersionedSchema и SchemaMigrationPlan. Код получается довольно объёмным, но зато у вас полный контроль над процессом:

enum TaskSchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }

    @Model
    class Task {
        var title: String
        var isCompleted: Bool
        init(title: String, isCompleted: Bool = false) {
            self.title = title
            self.isCompleted = isCompleted
        }
    }
}

enum TaskSchemaV2: VersionedSchema {
    static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }

    @Model
    class Task {
        var title: String
        var isCompleted: Bool
        var priority: Int
        init(title: String, isCompleted: Bool = false, priority: Int = 0) {
            self.title = title
            self.isCompleted = isCompleted
            self.priority = priority
        }
    }
}

enum TaskMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TaskSchemaV1.self, TaskSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: TaskSchemaV1.self,
        toVersion: TaskSchemaV2.self
    ) { context in
        // Произвольная логика миграции
        let tasks = try context.fetch(FetchDescriptor<TaskSchemaV1.Task>())
        for task in tasks {
            // Перенос данных или трансформация
        }
        try context.save()
    }
}

Затем подключите план миграции к контейнеру:

let container = try ModelContainer(
    for: TaskSchemaV2.Task.self,
    migrationPlan: TaskMigrationPlan.self
)

Наследование моделей — новинка iOS 26

Одно из самых ожидаемых нововведений SwiftData в iOS 26 (анонсировано на WWDC 2025) — поддержка наследования классов. До iOS 26 модели SwiftData не могли наследоваться друг от друга, и приходилось дублировать общие свойства. Это реально раздражало.

Как это работает

Теперь можно создавать полноценные иерархии моделей:

@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
class WorkEvent: Event {
    var meetingLink: String
    var attendees: [String]

    init(title: String, date: Date, location: String,
         meetingLink: String, attendees: [String] = []) {
        self.meetingLink = meetingLink
        self.attendees = attendees
        super.init(title: title, date: date, location: location)
    }
}

@available(iOS 26, *)
@Model
class SocialEvent: Event {
    var theme: String

    init(title: String, date: Date, location: String, theme: String) {
        self.theme = theme
        super.init(title: title, date: date, location: location)
    }
}

Запросы с наследованием

SwiftData поддерживает как «широкие» запросы по базовому классу (вернут все типы событий), так и «узкие» — по конкретному подклассу:

// Все события (и WorkEvent, и SocialEvent)
@Query private var allEvents: [Event]

// Только рабочие события — через предикат с оператором is
@Query(filter: #Predicate<Event> { $0 is WorkEvent })
private var workEvents: [Event]

Учтите: наследование моделей доступно только с iOS 26, поэтому все использования нового API нужно оборачивать в @available(iOS 26, *).

Синхронизация с CloudKit

Одна из сильных сторон SwiftData — встроенная поддержка синхронизации данных через iCloud с помощью CloudKit. Настройка на первый взгляд проста, но есть серьёзные ограничения, о которых лучше узнать до релиза, а не после.

Настройка

  1. Добавьте capability iCloud в настройках таргета Xcode
  2. Выберите CloudKit и создайте (или выберите существующий) контейнер
  3. Добавьте capability Background Modes и включите Remote Notifications
  4. Укажите cloudKitDatabase: .automatic в конфигурации контейнера
let config = ModelConfiguration(
    cloudKitDatabase: .automatic
)
let container = try ModelContainer(
    for: Task.self, Category.self,
    configurations: config
)

После этого SwiftData будет автоматически синхронизировать данные между устройствами пользователя. Звучит волшебно, но дьявол, как всегда, в деталях.

Обязательные правила для CloudKit-совместимых моделей

CloudKit накладывает жёсткие ограничения на модели данных. И вот что особенно неприятно — нарушение любого из них приводит к молчаливому отказу синхронизации без каких-либо ошибок в консоли:

  • Нельзя использовать @Attribute(.unique) — CloudKit не поддерживает атомарные проверки уникальности между устройствами
  • Все свойства должны быть опциональными или иметь значения по умолчанию
  • Все связи (relationships) должны быть опциональными
  • Нельзя использовать упорядоченные связи
// Правильно — совместимо с CloudKit
@Model
class CloudTask {
    var title: String = ""
    var notes: String?
    var isCompleted: Bool = false
    var category: Category?  // опциональная связь

    init(title: String = "", notes: String? = nil, isCompleted: Bool = false) {
        self.title = title
        self.notes = notes
        self.isCompleted = isCompleted
    }
}

Ограничения миграции при CloudKit

После публикации приложения в App Store с CloudKit-синхронизацией изменения схемы становятся крайне ограниченными. И это не шутка:

  • Нельзя удалять сущности или атрибуты — даже неиспользуемые
  • Нельзя переименовывать — CloudKit воспримет это как удаление + создание нового
  • Нельзя менять тип атрибута (например, String на Int)
  • Можно только добавлять новые сущности и атрибуты с значениями по умолчанию

Поэтому тщательно продумайте схему данных до первого релиза. Переделывать потом будет больно.

Особенности поведения синхронизации

Важно понимать: синхронизация SwiftData через CloudKit — это не реалтайм-база данных типа Firebase. Apple динамически регулирует частоту синхронизации в зависимости от состояния сети, заряда батареи и настроек пользователя. По поведению это ближе к тому, как синхронизируются «Заметки» или «Фото».

Тестировать синхронизацию лучше на реальных устройствах — в симуляторе она работает нестабильно. И ещё один подводный камень: если на macOS синхронизация работает в debug-режиме, но отказывает в TestFlight или App Store — проверьте, что CloudKit.framework добавлен в «Frameworks, Libraries, and Embedded Content». На это можно потратить несколько часов отладки (спрашивайте, откуда знаю).

SwiftData vs Core Data: когда что использовать

Выбор между SwiftData и Core Data в 2026 году зависит от конкретного проекта.

Выбирайте SwiftData, если:

  • Начинаете новый проект на SwiftUI с таргетом iOS 17+
  • Модель данных относительно простая
  • Хотите минимум шаблонного кода и максимум интеграции со SwiftUI
  • Нужна синхронизация с iCloud без долгой настройки

Оставайтесь на Core Data, если:

  • Поддерживаете приложение на UIKit с уже работающим Core Data
  • Нужны продвинутые возможности: NSFetchedResultsController, составные предикаты, производные атрибуты
  • Требуется поддержка iOS ниже 17
  • Работаете со сложной моделью данных с множеством связей
  • Критична максимальная производительность на больших объёмах данных

Гибридный подход

Поскольку SwiftData построен поверх Core Data, оба фреймворка могут мирно сосуществовать в одном проекте. Стратегия «Core Data для существующего, SwiftData для нового» позволяет мигрировать постепенно, без лишних рисков. А если вдруг окажется, что SwiftData не хватает какой-то возможности — переход обратно на Core Data пройдёт без потери данных, потому что под капотом это одна и та же база.

Практический пример: приложение «Менеджер задач»

Давайте соберём всё вместе и посмотрим, как выглядит структура реального приложения:

import SwiftUI
import SwiftData

// MARK: - Модели

@Model
class TaskCategory {
    var name: String
    var colorHex: String
    @Relationship(deleteRule: .cascade, inverse: \TodoItem.category)
    var items: [TodoItem]

    init(name: String, colorHex: String = "#007AFF") {
        self.name = name
        self.colorHex = colorHex
        self.items = []
    }
}

@Model
class TodoItem {
    var title: String
    var notes: String
    var isCompleted: Bool
    var dueDate: Date?
    var priority: Int
    var createdAt: Date
    var category: TaskCategory?

    init(title: String, notes: String = "", priority: Int = 0,
         dueDate: Date? = nil, category: TaskCategory? = nil) {
        self.title = title
        self.notes = notes
        self.isCompleted = false
        self.priority = priority
        self.dueDate = dueDate
        self.createdAt = Date()
        self.category = category
    }
}

// MARK: - Главный экран

struct TodoListView: View {
    @Environment(\.modelContext) private var context
    @State private var searchText = ""

    var body: some View {
        NavigationStack {
            FilteredTodoList(searchText: searchText)
                .navigationTitle("Мои задачи")
                .searchable(text: $searchText, prompt: "Поиск задач")
        }
    }
}

// MARK: - Отфильтрованный список

struct FilteredTodoList: View {
    @Environment(\.modelContext) private var context
    @Query private var items: [TodoItem]

    init(searchText: String) {
        let predicate = #Predicate<TodoItem> { item in
            searchText.isEmpty || item.title.localizedStandardContains(searchText)
        }
        _items = Query(
            filter: predicate,
            sort: [
                SortDescriptor(\TodoItem.isCompleted),
                SortDescriptor(\TodoItem.priority, order: .reverse),
                SortDescriptor(\TodoItem.createdAt, order: .reverse)
            ]
        )
    }

    var body: some View {
        List {
            ForEach(items) { item in
                TodoRowView(item: item)
            }
            .onDelete { indexSet in
                for index in indexSet {
                    context.delete(items[index])
                }
            }
        }
    }
}

Этот пример демонстрирует все ключевые паттерны: модели со связями, динамическую фильтрацию, множественную сортировку и удаление свайпом. Добавьте .modelContainer(for: [TaskCategory.self, TodoItem.self]) к корневому view — и приложение готово.

Часто задаваемые вопросы (FAQ)

Можно ли использовать SwiftData без SwiftUI?

Да, SwiftData работает в UIKit-приложениях и даже в серверном коде на Swift. Но макрос @Query доступен только внутри SwiftUI-view. Для UIKit используйте FetchDescriptor и ручную выборку через modelContext.fetch(). Автоматическое обновление UI придётся реализовывать самостоятельно — это, конечно, менее удобно.

Поддерживает ли SwiftData многопоточность?

ModelContext привязан к одному потоку (обычно @MainActor). Для фоновых операций создавайте отдельный контекст через ModelContainer и оборачивайте работу с ним в ModelActor. Это обеспечит безопасную конкурентную работу с данными.

Как протестировать код со SwiftData?

Используйте конфигурацию с isStoredInMemoryOnly: true для тестового контейнера. Данные живут только в памяти, и каждый тест стартует с чистого состояния. В SwiftUI-превью тоже рекомендуется in-memory контейнер — иначе превью будут работать заметно медленнее.

Какая минимальная версия iOS поддерживает SwiftData?

SwiftData требует iOS 17, macOS 14 (Sonoma), watchOS 10 или tvOS 17 и выше. Наследование моделей доступно начиная с iOS 26. Если нужна поддержка более ранних версий — Core Data или гибридный подход.

Как отлаживать запросы SwiftData?

Добавьте аргумент запуска -com.apple.CoreData.SQLDebug 1 в схеме Xcode (Product → Scheme → Edit Scheme → Arguments). В консоли появятся все SQL-запросы, которые генерирует SwiftData — это незаменимо для отладки проблем с производительностью. Для диагностики CloudKit-синхронизации используйте CloudKit Console на сайте Apple Developer.

Об авторе Mei-Lin Chen

Mei-Lin joined Robinhood in 2020 as an iOS engineer on the Crypto team and stayed through the SwiftUI rewrite of the order-entry flow before leaving in 2025. She also did a two-year stint at Asana earlier in her career working on the iPad app and the Mac Catalyst port. She writes about the parts of Apple's frameworks that the WWDC talks gloss over - what Observable actually does to your view-update graph, why @Bindable bindings tear in some animation contexts, and the surprisingly deep rabbit hole of Swift macros for boilerplate elimination. She has shipped two indie apps to the App Store, one of which hit #4 in the Health & Fitness category for a week in 2023. Mei-Lin is based in Seattle and has been writing Swift for 8 years.