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

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

Об авторе Editorial Team

Our team of expert writers and editors.