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
Networkingrequest, - 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.
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.
