Другое

CoreData automaticallyMergesChangesFromParent: Исправление двойных обновлений SwiftUI

Узнайте, почему автоматическиMergesChangesFromParent в CoreData вызывает двойные обновления представлений SwiftUI при сохранении данных. Найдите решения для предотвращения двойных обновлений при сохранении структуры стека Core Data для оптимальной производительности.

Почему автоматическиMergesChangesFromParent в CoreData вызывает двойное обновление представлений SwiftUI?

Я столкнулся с проблемой, когда представления SwiftUI обновляются дважды при сохранении данных с включенным automaticallyMergesChangesFromParent, установленным в true. Второе обновление происходит, когда сущность переходит в состояние fault.

Настройка CoreData Stack

Вот моя конфигурация CoreData:

swift
let mom = NSManagedObjectModel(contentsOf:modelURL)!
managedObjectContext = NSManagedObjectContext(concurrencyType:.mainQueueConcurrencyType)
privateContext = NSManagedObjectContext(concurrencyType:.privateQueueConcurrencyType)
coordinator = NSPersistentStoreCoordinator(managedObjectModel:mom)
managedObjectContext.automaticallyMergesChangesFromParent = true
privateContext.automaticallyMergesChangesFromParent = true
privateContext.persistentStoreCoordinator = coordinator
managedObjectContext.parent = privateContext
managedObjectContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
privateContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)

В этой настройке managedObjectContext используется для чтения/записи на Main Actor, в то время как privateContext обрабатывает запись/чтение из SQL хранилища.

Метод сохранения

Вот моя реализация сохранения:

swift
func save() {
    if !(managedObjectContext.hasChanges) {
        return
    }
    do {
        try self.managedObjectContext.safeSave()
        do {
            try self.privateContext.safeSave()
        }
        catch let error {
            // ОБРАБОТКА ОШИБКИ
        }
    }
    catch let error as NSError {
        // ОБРАБОТКА ОШИБКИ
    }
}

// .....

extension NSManagedObjectContext {
    func safeSave() throws {
        try performAndWait {
            if hasChanges {
                try save()
            }
        }
    }
}

Проблема

Когда я сохраняю сущность, любое SwiftUI представление, которое её наблюдает, обновляется дважды. Второе обновление происходит потому, что automaticallyMergesChangesFromParent установлено в true, и сущность переходит в состояние fault.

Например, это SwiftUI представление:

swift
struct CellView: View {
    @ObservedObject private var topic: TopicStatistic

    var body: some View {
        print("CELL BODY \(topic)")
        return Button {
            topic.isSelected = !topic.isSelected
            DataStack.shared.save() //вызывает метод сохранения, показанный выше
        } label: { Text("Test") }
    }
}

Выводит два сообщения “CELL BODY” при нажатии на кнопку:

  1. Первое обновление:
CELL BODY <Topic: 0x600002137b60> (entity: Topic; id: 0x85da4595cd988912 <x-coredata://F05EF0BE-58F4-423C-A12E-E4FF67BE17D5/Topic/p65>; data: {
    isSelected = 0;
    // другие свойства здесь
})
  1. Второе обновление (когда сущность переходит в состояние fault):
CELL BODY <Topic: 0x60000213dc20> (entity: Topic; id: 0x8db98711ee4d8ece <x-coredata://F05EF0BE-58F4-423C-A12E-E4FF67BE17D5/Topic/p53>; data: <fault>)

Что я проверил

  1. Удаление любого из настроек automaticallyMergesChangesFromParent решает проблему.
  2. Отсутствие использования “промежуточного” privateContext и прямое прикрепление managedObjectContext к постоянному хранилищу также решает проблему.

Однако ни один из этих вариантов не подходит для меня, так как мне нужно поддерживать эту структуру стека для фонового контекста, используемого с синхронизацией Cloud.

Вопрос

Почему объекты переходят в состояние fault, хотя я ожидал, что изменения будут распространяться в одном направлении к постоянному хранилищу при нажатии на кнопку? Как можно предотвратить двойные обновления, сохраняя структуру моего CoreData стека?

Двойные обновления происходят потому, что automaticallyMergesChangesFromParent запускает как немедленное слияние изменений, так и последующее поведение создания “фолтов” (faulting), когда Core Data понимает, что объекты необходимо обновить. Это создает два отдельных цикла обновления в представлениях SwiftUI.


Содержание


Понимание проблемы двойных обновлений

Явление двойных обновлений, с которым вы сталкиваетесь, является результатом взаимодействия механизма создания фолтов Core Data с автоматическим слиянием. Когда automaticallyMergesChangesFromParent включен, Core Data выполняет две отдельные операции, обе из которых вызывают обновления представлений SwiftUI:

  1. Первое обновление: Изменения сливаются из родительского контекста (privateContext) в дочерний контекст (managedObjectContext), что заставляет SwiftUI обнаружить изменения свойств и обновить представление.

  2. Второе обновление: Когда объединенные объекты доступны для доступа, Core Data понимает, что их необходимо перезагрузить из постоянного хранилища (поведение создания фолтов), что вызывает еще один цикл обновления.

Это поведение особенно заметно в вашей настройке, потому что вы используете отношения родитель-дочерний контекст, где оба контекста имеют automaticallyMergesChangesFromParent = true, создавая сложный сценарий слияния.


Механизм создания фолтов и слияния в Core Data

Core Data использует сложную систему создания фолтов для оптимизации производительности. Согласно документации Apple о Создании фолтов и обеспечении уникальности, “Если на каком-то этапе доступен к постоянному свойству объекта-фолта, Core Data автоматически извлекает данные для объекта и инициализирует его. Этот процесс обычно называется активацией фолта”.

Когда automaticallyMergesChangesFromParent установлен в true, Core Data автоматически сливает изменения из родительских контекстов в дочерние. Однако этот процесс слияния немедленно не обновляет все свойства объектов - он создает объекты-фолты, которые необходимо активировать при доступе.

swift
// Поведение создания фолтов:
// До создания фолта: <Topic: 0x600002137b60> (data: { ... })
// После создания фолта: <Topic: 0x60000213dc20> (data: <fault>)

Механизм создания фолтов важен для производительности, но вызывает проблему двойных обновлений при сочетании с автоматическим слиянием в представлениях SwiftUI.


Почему ваша конкретная настройка вызывает двойные обновления

Ваша конфигурация стека CoreData с вложенными контекстами и автоматическим слиянием создает идеальные условия для двойных обновлений:

Динамика отношений родитель-дочерний контекст

swift
managedObjectContext.parent = privateContext
managedObjectContext.automaticallyMergesChangesFromParent = true
privateContext.automaticallyMergesChangesFromParent = true

Эта настройка создает двунаправленный сценарий слияния, где:

  • Изменения текут из privateContextmanagedObjectContext (родительский в дочерний)
  • Изменения также пытаются течь из managedObjectContextprivateContext (дочерний в родительский, хотя это менее распространено)

Анализ последовательности сохранения

Ваш метод сохранения выполняется в двух фазах:

swift
func save() {
    // Фаза 1: Сохранение основного контекста
    try self.managedObjectContext.safeSave()
    
    // Фаза 2: Сохранение приватного контекста  
    try self.privateContext.safeSave()
}

Эта последовательность запускает:

  1. Первое обновление: managedObjectContext сохраняет изменения, которые затем сливаются в privateContext (из-за automaticallyMergesChangesFromParent)
  2. Второе обновление: Когда SwiftUI обращается к объекту, Core Data активирует фолт, реализуя обновленные данные

Взаимодействие политик слияния

Ваше использование NSMergeByPropertyObjectTrumpMergePolicyType для обоих контекстов может усугубить проблему, так как это определяет, как конфликтующие изменения разрешаются во время слияния.


Решения для предотвращения двойных обновлений

Решение 1: Корректировка иерархии контекстов

Измените иерархию контекстов, чтобы предотвратить автоматическое слияние в обоих направлениях:

swift
// Включайте автоматическое слияние только там, где это необходимо
managedObjectContext.automaticallyMergesChangesFromParent = true
privateContext.automaticallyMergesChangesFromParent = false  // Отключите для приватного контекста

Решение 2: Ручное управление слиянием

Вместо зависимости от автоматического слияния реализуйте ручное управление слиянием:

swift
func save() {
    if managedObjectContext.hasChanges {
        try managedObjectContext.performAndWait {
            try managedObjectContext.save()
        }
    }
    
    if privateContext.hasChanges {
        try privateContext.performAndWait {
            try privateContext.save()
        }
        // Ручное слияние изменений в контекст представления
        managedObjectContext.mergeChanges(fromContextDidSave: privateContext.notificationCenter.post(
            name: .NSManagedObjectContextDidSave,
            object: privateContext
        ))
    }
}

Решение 3: Предотвращение создания фолтов

Предотвращайте ненужное создание фолтов, обращаясь к свойствам объектов перед обновлением SwiftUI:

swift
struct CellView: View {
    @ObservedObject private var topic: TopicStatistic

    var body: some View {
        // Обращение к свойствам для предотвращения создания фолтов
        let _ = topic.isSelected
        let _ = topic.someOtherProperty
        
        return Button {
            topic.isSelected = !topic.isSelected
            DataStack.shared.save()
        } label: { Text("Test") }
    }
}

Решение 4: Изоляция контекстов

Рассмотрите возможность использования отдельных контекстов без отношений родитель-дочерний:

swift
// Вместо родитель-дочерний, используйте отдельные контексты
let viewContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)

// Оба контекста подключаются напрямую к одному координатору
viewContext.persistentStoreCoordinator = coordinator
backgroundContext.persistentStoreCoordinator = coordinator

Лучшие практики управления контекстами

Лучшие практики для иерархии контекстов

  1. Простая иерархия: Используйте минимально необходимое количество отношений контекстов
  2. Направленное слияние: Включайте автоматическое слияние только в одном направлении (родительский → дочерний)
  3. Четкое разделение: Используйте приватные контексты для фоновых операций и основные контексты для пользовательского интерфейса

Оптимизация стратегии слияния

swift
// Рекомендуемая конфигурация политики слияния
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
privateContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

// Рассмотрите возможность использования пользовательских политик слияния для сложных сценариев
privateContext.mergePolicy = NSMergePolicy(merge: .customMergePolicyType)

Обновления на основе уведомлений

Вместо зависимости от автоматического слияния используйте обновления на основе уведомлений:

swift
private var observerTokens: [NSObjectProtocol] = []

func setupContextNotifications() {
    let token = NotificationCenter.default.addObserver(
        forName: .NSManagedObjectContextDidSave,
        object: privateContext,
        queue: .main
    ) { [weak self] notification in
        self?.managedObjectContext.mergeChanges(fromContextDidSave: notification)
    }
    observerTokens.append(token)
}

Альтернативные подходы

Подход 1: Один контекст с фоновыми операциями

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

swift
let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

// Для фоновых операций
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
    let context = mainContext.newBackgroundContext()
    context.perform {
        block(context)
        try? context.save()
        mainContext.mergeChanges(fromContextDidSave: context.notificationCenter.post(
            name: .NSManagedObjectContextDidSave,
            object: context
        ))
    }
}

Подход 2: Интеграция SwiftUI + Combine

Используйте издателей (publishers) Combine для более контролируемых обновлений:

swift
class CoreDataPublisher: ObservableObject {
    @Published var topics: [Topic] = []
    
    func observeChanges(in context: NSManagedObjectContext) {
        context.perform { [weak self] in
            let notification = NotificationCenter.default.publisher(
                for: .NSManagedObjectContextDidSave,
                object: context
            )
            .sink { _ in
                self?.refreshTopics()
            }
        }
    }
    
    private func refreshTopics() {
        // Извлечение и публикация обновлений
    }
}

Подход 3: Устойчивость к фолтам в представлениях

Сделайте ваши представления SwiftUI более устойчивыми к фолтам:

swift
struct FaultTolerantView: View {
    @ObservedObject var topic: TopicStatistic
    
    var body: some View {
        Group {
            if topic.isFault {
                ProgressView("Загрузка...")
            } else {
                actualContent
            }
        }
    }
    
    private var actualContent: some View {
        Button {
            topic.isSelected = !topic.isSelected
            DataStack.shared.save()
        } label: { Text("Test") }
    }
}

Заключение

Двойные обновления, с которыми вы сталкиваетесь, возникают из-за взаимодействия механизма создания фолтов Core Data с автоматическим слиянием в иерархиях контекстов родитель-дочерний. Чтобы решить эту проблему, сохраняя структуру вашего стека:

  1. Отключите автоматическое слияние в вашем приватном контексте, чтобы предотвратить двунаправленное слияние
  2. Реализуйте ручное управление слиянием для более точного управления обновлениями
  3. Обращайтесь к свойствам объектов перед обновлением SwiftUI, чтобы предотвратить создание фолтов
  4. Рассмотрите изоляцию контекстов, если сложность становится неуправляемой

Ключ заключается в нахождении правильного баланса между автоматическим удобством и ручным контролем. Для сценариев синхронизации с облаком подход с ручным слиянием часто обеспечивает наиболее предсказуемое поведение, сохраняя преимущества производительности.

Всегда тщательно тестируйте ваше решение, так как оптимальный подход может варьироваться в зависимости от конкретных требований вашего приложения и шаблонов доступа к данным.

Авторы
Проверено модерацией
Модерация