1

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.

Russ
  • 467
  • 3
  • 10

2 Answers2

1

It depends on if it's for examining Production or Debug builds in the system's Console or Xcode's Console respectively.

For Production builds, my understanding is the aim is to make my messages more findable (rather than de-emphasising/hiding other messages) by consistently using something like:

let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "YourCategorisationOfMessagesGoingToThisHandle")

and then in the code I might have things like

log.debug("My debug message") log.warning("My warning etc")

fwiw: I tend to categorise stuff by the file it's in, as that's deterministic and helps me find the file, so my source files tend to start with

fileprivate let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #file.components(separatedBy: "/").last ?? "")

If I do this, then I can easily filter the system's console messages to find stuff that's relevant to my app.

There's more on how to use this and the console to filter for the app's messages in the sytem console over here.

For Debug builds and the Xcode console the same consistent app log messages from my app could be used, e.g. my app's debug messages always start with "Some easily findable string or other". I don't believe there is a way to throttle/cut-off responses selectively. But it definitely possible to turn off debug messages from many of the noisy sub-systems completely (once happy that they are working reliably)

For Core Data and CloudKit cases mentioned, if I run the Debug builds with the -com.apple.CoreData.Logging.stderr 0 and -com.apple.CoreData.CloudKitDebug 0 launch args then that make Xcode's console a lot quieter :-). Nice instructions on how to set this up in the SO answer over here

shufflingb
  • 1,847
  • 16
  • 21
0

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.

Russ
  • 467
  • 3
  • 10