NSFetchedResultsController doesn't update data when it saved to CoreData

38 views Asked by At

My app uses NSFetchedResultsController to show data, which is stored in a shared CoreData container. I use AppGroups to share my CoreData container with different app targets.

Recently I added a new target - UNNotificationServiceExtension to the app. In a corresponding didReceive method I:

  • make a Networking request,
  • save new data to a shared CoreData
  • modify push's text with that data
  • then show push to user

If I send push when app is terminated, and open it - NSFetchedResultsController shows new data, which was added from UNNotificationServiceExtension networking request.

The problem occurs when push arrives with app is in foreground or background and not yet terminated - NSFetchedResultsController just continues to show old data.

I checked CoreData sqlite database with another app on my Mac, and I see that after UNNotificationServiceExtension networking new data successfully saved and persist in CoreData, but to be able to display it in ViewController I should reopen the app.

And If I do Networking in ViewController directly, it saves data to CoreData with the same method I use in UNNotificationServiceExtension and changes displayed straight away.

I tried to print values on viewWillAppear in my ViewController and it always fetches the old data which was stored before networking request from UNNotificationServiceExtension...

let currencies = coreDataManager.fetchCurrencies(entityName: Currency.self)
let filtered = currencies.filter {$0.shortName == "EUR"}
print(filtered.first!.currentValue) 
//prints old data

 func fetchCurrencies<T: NSFetchRequestResult>(entityName: T.Type, predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil) -> [T] {
    var fetchedCurrencies: [T] = []
    let request = NSFetchRequest<T>(entityName: String(describing: entityName))
    let context = PersistenceController.shared.container.viewContext
    
    if (predicate != nil) && (sortDescriptors != nil) {
        request.predicate = predicate
        request.sortDescriptors = sortDescriptors
    }
    
    do {
        fetchedCurrencies = try context.fetch(request)
    } catch {
        print(error)
    }
    return fetchedCurrencies
}

I can't figure out why it shows the old data, despite sqlite database shows it should be a new data.

problem

I tried to recreate NSFetchedResultsController via viewWillAppear, but no luck:

func setupFetchedResultsController() {

        var currencyScreenViewPredicate: NSCompoundPredicate {
            let firstPredicate = NSPredicate(format: "isForCurrencyScreen == YES")
            let secondPredicate = NSPredicate(format: "isBaseCurrency == NO")
            return NSCompoundPredicate(type: .and, subpredicates: [firstPredicate, secondPredicate])
        }
        
        var sortDescriptor: NSSortDescriptor {
                return NSSortDescriptor(key: "rowForCurrency", ascending: true)
        }
      
        currencyFRC = coreDataManager.createCurrencyFRC(with: currencyScreenViewPredicate, and: sortDescriptor)
        currencyFRC.delegate = self
        try? currencyFRC.performFetch()
    
    tableView.reloadData()
}

Here is how I create a FetchedResultsController:

func createCurrencyFRC(with predicate: NSPredicate? = nil, and sortDescriptor: NSSortDescriptor? = nil) -> NSFetchedResultsController<Currency> {

    let request: NSFetchRequest<Currency> = Currency.fetchRequest()
    let baseSortDescriptor = NSSortDescriptor(key: "shortName", ascending: true)
    request.predicate = predicate

    return NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}

Here is my shared Persistence Class (here I also migrated to App Group):

import CoreData

struct PersistenceController {
static let shared = PersistenceController()
let databaseName = "Kursvalut.sqlite"

var oldStoreURL: URL {
    let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
    return directory?.appendingPathComponent(databaseName) ?? URL(string: "")!
}

var sharedStoreURL: URL {
    let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.ru.igorcodes.Kursvalut")
    return container?.appendingPathComponent(databaseName) ?? URL(string: "")!
}

let container: NSPersistentContainer

init(inMemory: Bool = false) {
    container = NSPersistentContainer(name: "Kursvalut")
    
    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    } else if !FileManager.default.fileExists(atPath: oldStoreURL.path) {
       container.persistentStoreDescriptions.first!.url = sharedStoreURL
    }
    //print("Container URL equals: \(container.persistentStoreDescriptions.first!.url!)")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    migrateStore(for: container)
    container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
    container.viewContext.automaticallyMergesChangesFromParent = true
}

func migrateStore(for container: NSPersistentContainer) {
    let coordinator = container.persistentStoreCoordinator
    guard let oldStore = coordinator.persistentStore(for: oldStoreURL) else { return }
    
    do {
        try coordinator.migratePersistentStore(oldStore, to: sharedStoreURL, options: nil, withType: NSSQLiteStoreType)
    } catch {
        print("Unable to migrate to shared store with error: \(error.localizedDescription)")
    }
    removeOldStore()
}

func removeOldStore() {
    do {
        try FileManager.default.removeItem(at: oldStoreURL)
    } catch {
        print("Unable to delete old store")
    }
}

func saveContext () {
    if container.viewContext.hasChanges {
        do {
            try container.viewContext.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}
}

I have setup all needed NSFetchedResultsController methods in ViewController. It all works when I make networking from VC:

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
}

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
}

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .update:
        if let indexPath = indexPath {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                self.tableView.reloadRows(at: [indexPath], with: .none)
            }
        }
    case .move:
        if let indexPath = indexPath, let newIndexPath = newIndexPath {
            tableView.moveRow(at: indexPath, to: newIndexPath)
        }
    case .delete:
        if let indexPath = indexPath {
            tableView.deleteRows(at: [indexPath], with: .none)
        }
    case .insert:
        if let newIndexPath = newIndexPath {
            tableView.insertRows(at: [newIndexPath], with: .none)
        }
    default:
        tableView.reloadData()
    }
}

If I make Networking request from ViewController it then will show the new data. It can be a workaround, but I don't know how to notify the ViewController to make a Networking request from another Target...

Hope someone can give an idea of how to track the issue.

0

There are 0 answers