Search code examples
core-dataswiftuisynchronizationicloudcloudkit

Too Many Notifications CoreData/CloudKit Sync


I have several apps that use CoreData / iCloud syncing and they all receive a slew of update/change/insert/delete/etc notifications when they start and sometimes as they are running without any changes to the underlying data. When a new item is added or deleted, it appears that I get notifications for everything again. Even the number of notifications are not consistent.

My question is, how do I avoid this? Is there a cut-off that can be applied once I'm sure I have everything up to date on a device by device basis.

Persistence

import Foundation
import UIKit
import CoreData

struct PersistenceController {
    let ns = NotificationStuff()
    static let shared = PersistenceController()

    static var preview: PersistenceController = {
        let result = PersistenceController(inMemory: true)
        let viewContext = result.container.viewContext
        for _ in 0..<10 {
            let newItem = Item(context: viewContext)
            newItem.stuff = "Stuff"
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "TestCoreDataSync")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
    }
}

class NotificationStuff
{
    var changeCtr = 0
    
    init()
    {
        NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: Notification.Name.NSPersistentStoreRemoteChange, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(contextDidSave(_:)), name: Notification.Name.NSManagedObjectContextDidSave, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(contextObjectsDidChange(_:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
    }
    
    @objc func processUpdate(_ notification: Notification)
    {
        //print(notification)
        DispatchQueue.main.async
        { [self] in
            observerSelector(notification)
        }
    }
    
    @objc func contextObjectsDidChange(_ notification: Notification)
    {
       DispatchQueue.main.async
        { [self] in
            observerSelector(notification)
        }
    }
    
    @objc func contextDidSave(_ notification: Notification)
    {
        DispatchQueue.main.async
        {
            self.observerSelector(notification)
        }
    }
    
    func observerSelector(_ notification: Notification) {
        
        DispatchQueue.main.async
        { [self] in
            if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>, !insertedObjects.isEmpty
            {
                print("Insert")
            }
            
            if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>, !updatedObjects.isEmpty
            {
                changeCtr = changeCtr + 1
                print("Change \(changeCtr)")
            }
            
            if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>, !deletedObjects.isEmpty
            {
                print("Delete")
            }
            
            if let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject>, !refreshedObjects.isEmpty
            {
                print("Refresh")
            }
            
            if let invalidatedObjects = notification.userInfo?[NSInvalidatedObjectsKey] as? Set<NSManagedObject>, !invalidatedObjects.isEmpty
            {
                print("Invalidate")
            }
            
            
            let mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
            guard let context = notification.object as? NSManagedObjectContext else { return }
            
            // Checks if the parent context is the main one
            if context.parent === mainManagedObjectContext
            {
                
                // Saves the main context
                mainManagedObjectContext.performAndWait
                {
                    do
                    {
                        try mainManagedObjectContext.save()
                    } catch
                    {
                        print(error.localizedDescription)
                    }
                }
            }
        }
    }
}

ContentView

import SwiftUI
import CoreData

struct ContentView: View {
    @State var stuff = ""
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        VStack
        {
            TextField("Type here", text: $stuff,onCommit: { addItem(stuff: stuff)
                stuff = ""
            })
            List {
                ForEach(items) { item in
                    Text(item.stuff ?? "??")
                }
                .onDelete(perform: deleteItems)
            }
        }.padding()
    }

   private func addItem(stuff: String) {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            newItem.stuff = stuff

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

The database has an Item entity with a timestamp field and a string field named stuff.


Solution

  • My problem was that CoreData -> CloudKit integration was re-synching the same items over and over, thus the notifications. I discovered that I needed to add a sorted index for the modifiedTimestamp on all entities. Now things are much faster and few if any re-synched items.