Refreshing Core Data after changes from main app or extension

1.6k Views Asked by At

I have been working on a today widget for my Core Data app. I use a NSFetchedResultsController for the widget and main app and am notified when the data changed/added from the opposite target using UserDefaults and help from this post.

But updating the data or the NSFetchedResultsController to see the changes/additions does not work. I have tried:

  • refetching the data from the NSFetchedResultsController
  • setting the persistent container's view context stalenessInterval to 0 and calling viewContext.refreshAllObjects() then try to refetch the data (and without)
  • setting shouldRefreshRefetchedObjects to true on the fetchedController so it will automatically call.

I know the data is being saved because if I force quit the app or re-run the widget the new data is there. But I can not figure out how to refresh the app or the widget when the opposite has changed something.

I have been looking for a solution for the past couple of days and this is literally the last thing I need to be done with this widget. Please if anyone knows how to do this please help!

How I set up my NSFetchedResultsController:

lazy var fetchedResultsController: NSFetchedResultsController<Item> = setupFetchedController()

override func viewDidAppear(_ animated: Bool) {
        loadData()

        //check to see if any data was changed. Being notified about changes works every time
        subscribeForChangesObservation()
}


private func setupFetchedController() -> NSFetchedResultsController<Item> {
        let managedContext = CoreDataManager.sharedManager.persistentContainer.viewContext

        let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
        let request : NSFetchRequest<Item> = Item.fetchRequest()
        request.predicate = NSPredicate(format: "date <= %@", Date() as NSDate)
        request.sortDescriptors = [sortDescriptor]

        fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedContext, sectionNameKeyPath: nil, cacheName: nil)
        fetchedResultsController.delegate = self

        return fetchedResultsController
}

private func loadData() {
        do {
            try fetchedResultsController.performFetch()
            updateSnapshot()
        } catch {
            print("Hey Listen! Error performing fetchedResultsController fetch: \(error)")
        }
}

//reloads the items in the table
func updateSnapshot() {
        let fetchedItems = fetchedResultsController.fetchedObjects ?? []
        var snapshot = NSDiffableDataSourceSnapshot<Int, Item>()
        snapshot.appendSections([0])
        snapshot.appendItems(fetchedItems)
        dataSource.apply(snapshot, animatingDifferences: true)
}

I use diffable data sources (came out with iOS 13) with the NSFetchedResultsController, but I don't this doesn't have anything to do with the problem, because I tried without it and the same issue happens.

How I set up Core Data:

class CoreDataManager {
    static let sharedManager = CoreDataManager()
    private init() {}

    lazy var persistentContainer: NSPersistentContainer = {
        var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")

        //Get the correct container
        let containerToUse: NSPersistentContainer?
        if useCloudSync {
           //custom container to just set the defaultDirectoryURL to the app group url
           containerToUse = GroupedPersistentCloudKitContainer(name: "App")
        } else {
            containerToUse = NSPersistentContainer(name: "App")      
        }

        guard let container = containerToUse else {
            fatalError("Couldn't get a container")
        }

        //Set the storeDescription
        let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.App")!.appendingPathComponent("\(container.name).sqlite")

        var defaultURL: URL?
        if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
            defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
        }

        if defaultURL == nil {
            container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
        }


        let description = container.persistentStoreDescriptions.first else {
            fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
        }

        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        if !useCloudSync {
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        }

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in

            //migrate from old url to use app groups
            if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
                let coordinator = container.persistentStoreCoordinator
                if let oldStore = coordinator.persistentStore(for: url) {
                    do {
                        try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
                    } catch {
                        print("Hey Listen! Error migrating persistent store")
                        print(error.localizedDescription)
                    }

                    // delete old store
                    let fileCoordinator = NSFileCoordinator(filePresenter: nil)
                    fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
                        do {
                            try FileManager.default.removeItem(at: url)
                        } catch {
                            print("Hey Listen! Error deleting old persistent store")
                            print(error.localizedDescription)
                        }
                    })
                }
            }
         }

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.transactionAuthor = appTransactionAuthorName

        // Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
        container.viewContext.automaticallyMergesChangesFromParent = true
        do {
            try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
            fatalError("Hey Listen! ###\(#function): Failed to pin viewContext to the current generation:\(error)")
        }

        // Observe Core Data remote change notifications.
        NotificationCenter.default.addObserver(
            self, selector: #selector(type(of: self).storeRemoteChange(_:)),
            name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)

         return container
   }
}
1

There are 1 best solutions below

5
Ely On

Have you included this in your viewContext setup?

persistentContainer.viewContext.automaticallyMergesChangesFromParent = true