NSFetchedResultsController and to-many relationship not working - core-data

Ok, I was searching and trying in that case for the last 1-2 weeks and I didn't get it work. I would be able to achieve what I want without NSFRC but for performance reasons and convienience I would like to do it with the NSFRC.
So, I have a DataModel with 2 Entities - see the picture
There is one Account and one account can have many accountchanges - which is quite obvious.
So I want to be able to choose an Account and then show all AccountChanges for that specific Account.
So far I was able to get the Account and also accessing the NSSet in cellForRow Function but I am not getting the correct sections and numberOfRowsInSection - this is the main issue.
Here is some code:
func numberOfSections(in tableView: UITableView) -> Int {
print("Sections : \(self.fetchedResultsController.sections?.count)")
if (self.fetchedResultsController.sections?.count)! <= 0 {
print("There are no objects in the core data - do something else !!!")
}
return self.fetchedResultsController.sections?.count ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Section Name")
print(self.fetchedResultsController.sections![section].name)
let sectionInfo = self.fetchedResultsController.sections![section]
print("Section: \(sectionInfo) - Sections Objects: \(sectionInfo.numberOfObjects)")
return sectionInfo.numberOfObjects
}
There are some print statements which are only for information!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let myCell = myTable.dequeueReusableCell(withIdentifier: "myCell")! as UITableViewCell
let accountBalanceChanges = self.fetchedResultsController.object(at: indexPath)
print("AccountBalanceChanges from cell....")
print(accountBalanceChanges)
let details = accountBalanceChanges.accountchanges! as NSSet
print("Print out the details:")
print(details)
let detailSet = details.allObjects
let detailSetItem = detailSet.count // Just for information!
let myPrint = detailSet[indexPath.row] as! AccountChanges
let myVal = myPrint.category
myCell.textLabel?.text = myVal
return myCell
}
So, I am able to get the data but always only one item and not the whole set - I guess due to the fact that the sections/ numberOfRows are wrong.
Here is my NSFRC
var fetchedResultsController: NSFetchedResultsController<Accounts> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<Accounts> = Accounts.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "aName", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate = NSPredicate(format: "(ANY accountchanges.accounts = %#)", newAccount!)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
return _fetchedResultsController!
}
I am assuming it is the SortDescriptor or the predicate - or maybe both?
Any help or at least directions are well appreciated.
I already tried many different approaches but none was giving me the correct results.

I would do the opposite, I mean using the FRC to fetch all the changes for an account with a certain Id, and use the following predicate:
let predicate = NSPredicate(format: "accounts.aId = %#", ACCOUNTID)
or
let predicate = NSPredicate(format: "accounts = %#", account.objectID)
I would rename Accounts entity to Account and same for the relationship since it's a to-one relationship.
That's assuming you have a table view with all the accounts and when you click on one it gives you back its changes.
var fetchedResultsController: NSFetchedResultsController<AccountChanges> {
if _fetchedResultsController != nil {
return _fetchedResultsController!
}
let fetchRequest: NSFetchRequest<AccountChanges> = AccountChanges.fetchRequest()
// Set the batch size to a suitable number.
fetchRequest.fetchBatchSize = 20
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: "aName", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
let predicate = NSPredicate(format: "accounts.aId = %#", ACCOUNTID)
fetchRequest.predicate = predicate
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
aFetchedResultsController.delegate = self
_fetchedResultsController = aFetchedResultsController
do {
try _fetchedResultsController!.performFetch()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
return _fetchedResultsController!
}
Cheers

Related

Fetching objects form core data by IndexPath

I have a TableView. By tapping an entry in this table, another view opens in which that entry is editable.
What's the proper way to handle this? I am aware that I can fetch entries from Core Data via predicates like this:
func fetchEntriesStartingWith(startingDate:Date) -> [FuelEntry] {
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "FuelEntry")
fetchRequest.predicate = NSPredicate(format: "date >= %#", startingDate as CVarArg)
let sort = NSSortDescriptor(key: #keyPath(FuelEntry.date), ascending: false)
fetchRequest.sortDescriptors = [sort]
do {
return try (CoreDataHandler.managedContext.fetch(fetchRequest) as? [FuelEntry] ?? [FuelEntry]())
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return []
}
}
However, this seems error prone: What if I have two entries with the exact same values (might happen in my application) and thus, the NSFetchRequest returns two entries?
Obviously, those two entries will have different IDs, but I'm unsure how to work with those and infer which entry will correspond to what indexPath.

NSFetchedResultsController can't update the tableView swift4

I'm trying to make it work for last couple of days and can't get it working. Its something tiny detail obviously I can't seem to find.
Could you take a look and give me some insights about my code?
I'm trying to update the logView with app savings in the coredata.
Here's the entire code for ViewController and CoreData Handler.
/// fetch controller
lazy var fetchController: NSFetchedResultsController = { () -> NSFetchedResultsController<NSFetchRequestResult> in
let entity = NSEntityDescription.entity(forEntityName: "Logs", in: CoreDataHandler.sharedInstance.backgroundManagedObjectContext)
let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
fetchRequest.entity = entity
let nameDescriptor = NSSortDescriptor(key: "name", ascending: false)
fetchRequest.sortDescriptors = [nameDescriptor]
let fetchedController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataHandler.sharedInstance.backgroundManagedObjectContext, sectionNameKeyPath: "duration", cacheName: nil)
fetchedController.delegate = self as? NSFetchedResultsControllerDelegate
return fetchedController
}()
override func viewDidLoad() {
title = "Week Log"
tableView.tableFooterView = UIView(frame: CGRect.zero)
tableView.separatorColor = UIColor.black
tableView.backgroundColor = UIColor.red
refreshView()
loadNormalState()
loadCoreDataEntities()
}
/**
Refresh the view, reload the tableView.
*/
func refreshView() {
loadCoreDataEntities()
tableView.reloadData()
}
/**
Load history entities from core data. (I'm printing on the console and
be able to see the the fetched data but I can't load it to tableView.)
*/
func loadCoreDataEntities() {
do {
try fetchController.performFetch()
} catch {
print("Error occurred while fetching")
}
}
import Foundation
import CoreData
class CoreDataHandler: NSObject {
/**
Creates a singleton object to be used across the whole app easier
- returns: CoreDataHandler
*/
class var sharedInstance: CoreDataHandler {
struct Static {
static var instance: CoreDataHandler = CoreDataHandler()
}
return Static.instance
}
lazy var backgroundManagedObjectContext: NSManagedObjectContext = {
let backgroundManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let coordinator = self.persistentStoreCoordinator
backgroundManagedObjectContext.persistentStoreCoordinator = coordinator
return backgroundManagedObjectContext
}()
lazy var objectModel: NSManagedObjectModel = {
let modelPath = Bundle.main.url(forResource: "Model", withExtension: "momd")
let objectModel = NSManagedObjectModel(contentsOf: modelPath!)
return objectModel!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.objectModel)
// Get the paths to the SQLite file
let storeURL = self.applicationDocumentsDirectory().appendingPathComponent("Model.sqlite")
// Define the Core Data version migration options
let options = [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption: true]
// Attempt to load the persistent store
var error: NSError?
var failureReason = "There was an error creating or loading the application's saved data."
do {
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return persistentStoreCoordinator
}()
func applicationDocumentsDirectory() -> NSURL {
return FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last! as NSURL
}
func saveContext() {
do {
try backgroundManagedObjectContext.save()
} catch {
print("Error while saving the object context")
// Error occured while deleting objects
}
}
You have a data source delegate somewhere. That data source delegate tells the table view how many items there are, and what their contents is. How does it know how many items? That must be stored somewhere.
When the fetch controller is successful, it must modify the data that the data source delegate relies on in some way, and then call reloadData. Are you doing this? Are you doing anything that causes the data source delegate to change the number of items it reports?
And calling loadCoreDataEntities, immediately followed by reloadData, is nonsense. loadCoreDataEntities is asynchronous. By the time you call reloadData, it hasn't loaded any entities yet. realodData is called when loadCoreDataEntities has finished.

doubled savings with core data swift 4

developing a little app for my comic collection encountered this issue:
in my second "add comic" VC I have a button and the func below, but I save TWICE entities in manged context (ate least, I think this is the issue)
for example if I have 2 comics yet shown in main VC tableview, go to "add comic VC" and save a third one, going back to main VC I'll print 3 objects with title, number etc but also print 2 new objects with no data as I had saved twice a manger context a "right one" and another one with same number of object but empty. If I keep adding a 4th comic, I'll get 6 complete comic + the 4th and more 6 "blank itmes" with default values "no title"
let kComicEntityName = "Comic"
func addingSingleComic(gotTitle: String, gotIssue: Int16, gotInCollection: Bool ) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: kComicEntityName, in: managedContext)!
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
comicToAdd.comicTitle = gotTitle
comicToAdd.issueNumber = gotIssue
comicToAdd.inCollection = gotInCollection
do {
try managedContext.save()
} catch let error as NSError {
print("could not save. \(error), \(error.userInfo)")
}
print("new single comic crated: title: \(comicToAdd.comicTitle ?? "!! not title !!"), n. \(comicToAdd.issueNumber), owned?: \(comicToAdd.inCollection)")
}
in the main VC I use this to check items in core data
func asyncPrintEntities() {
self.asyncComicEntityArray.removeAll(keepingCapacity: true)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {return}
let managedContext = appDelegate.persistentContainer.viewContext
let comicFetch : NSFetchRequest<Comic> = Comic.fetchRequest()
asyncFetchRequest = NSAsynchronousFetchRequest<Comic>(fetchRequest: comicFetch) {
[unowned self] (result: NSAsynchronousFetchResult) in
guard let AllComicEntityResult = result.finalResult else {
return
}
self.asyncComicEntityArray = AllComicEntityResult
//************************************
do {
self.asyncComicEntityArray = try managedContext.fetch(comicFetch)
if self.asyncComicEntityArray.count > 0 {
print("Ok! model is not empty!")
} else {
print("No entites availabe")
}
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
guard self.asyncComicEntityArray != nil else {return}
for comicFoundInArray in self.asyncComicEntityArray {
let entity = NSEntityDescription.entity(forEntityName: self.kComicEntityName, in: managedContext)!
var comicTouse = Comic(entity: entity, insertInto: managedContext)
// var comicTouse = Comic() //to be deleted since this kind of init is not allowed, better above insertInto
comicTouse = comicFoundInArray as! Comic
print("comic title: \(comicTouse.comicTitle ?? "error title"), is it in collection? : \(comicTouse.inCollection)")
}
self.MyTableView.reloadData()
//************************************
}
// MARK: - async fetch request 3
do {
try managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
//end of function
}
In your addingSingleComic you create a new Comic here:
let comicToAdd = Comic(entity: entity, insertInto: managedContext)
Then you assign values to the object's properties.
Separately, in asyncPrintEntities, you also create new Comic objects here:
var comicTouse = Comic(entity: entity, insertInto: managedContext)
This time you do not assign values to the object's properties. They will have no title, etc, because you created them but never assigned a title. This line executes once for every object in asyncComicEntityArray, so if the array has two objects, you create two new objects that contain no data. You don't use comicToUse anywhere except in the one print, but it still exists in the managed object context and will still get saved the next time you save changes.
This is why you're getting extra entries-- because you're creating them in this line of code. It's not clear why you're creating them here. You just executed a fetch request, and then you immediately create a bunch of no-data entries which you don't use. It looks like that entire for loop could just be deleted, because the only thing it does is create these extra entries.

NSBatchUpdateRequest not making updates

I'm trying to do a batch update and I'm not getting an exception, but it's telling me 0 records are updated. If I run the following code
let request = NSBatchUpdateRequest(entityName: "Order")
request.predicate = NSPredicate(format: "sql_ident IN %#", completed)
request.propertiesToUpdate = ["complete" : true]
request.resultType = .UpdatedObjectsCountResultType
do {
let res = try moc.executeRequest(request) as! NSBatchUpdateResult
print("Set \(res.result!) objects to complete (should be \(completed.count))")
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
let r = NSFetchRequest(entityName: "Order")
r.predicate = NSPredicate(format: "sql_ident IN %#", completed)
let count = moc.countForFetchRequest(r, error: nil)
print("I found \(count) orders")
Then I get this output:
Set 0 objects to complete (should be 6470)
I found 6470 orders
So it's definitely seeing the objects I expect it to, but it's not updating any of them. What am I doing wrong here?
If it makes any difference, this is in a child context

NSFetchRequest with NSPredicate returning correct results, but whose properties aren't updated

My managedObjectContext hierarchy is as follows: (PSC)<-(writerMOC -- private)<-(mainMOC -- main)<-(backgroundMOC -- private)
I have an NSManagedObject who "name" property is "Banana".
In the backgroundMOC, I get a reference to the object with backgroundMOC.objectWithID, change the NSManagedObject's "name" property to "Apple", and subsequently set it's "syncStatus" property to 1 (flagged for synchronization), then recursively save the moc's with the following routine:
func saveManagedContext(moc: NSManagedObjectContext, shouldSync: Bool = true, completion: (() -> Void)? = nil)
{
print("\nSaving managed object context...")
do {
try moc.save()
if let parentContext = moc.parentContext {
parentContext.performBlock {
self.saveManagedContext(parentContext, shouldSync: shouldSync, completion: completion)
}
}
else {
if shouldSync { SyncEngine.sharedInstance.synchronize(shouldPushUpdates: true) }
completion?()
}
print("Finished saving managed object context...")
} catch {
logger.error("\(error)")
}
}
Once the last moc is saved, a sync routine is called which does its work on the backgroundMOC, which queries the local store for all objects whose syncStatus is 1, again this fetch is called on the backgroundMOC.
let fetchRequest = NSFetchRequest(entityName: entity.name)
let syncPredicate = NSPredicate(format: "%K == %d", JSONKey.SyncStatus.rawValue, 1)
fetchRequest.predicate = syncPredicate
return try backgroundMOC.executeFetchRequest(fetchRequest) as? [SyncableManagedObject] ?? []
This correctly returns the updated object in the array, however, that object's syncStatus property equals 0, and its "name" property is still set to "Banana".
This is really causing me headaches, I felt like i had totally understood how managedObjectContext blocks should work, but this has proven to be quite a puzzle.
UPDATE
Here's the code that prompts the update. This is called from the main thread when the cell is tapped.
func updateNameForCell(cell: UITableViewCell)
{
///gets the object id from the fetchedResultsController
guard let fruitMetaID = tableController.objectIDForCell(cell) else { return }
let backgroundMOC = CoreDataController.sharedInstance.newBackgroundManagedObjectContext()
backgroundMOC.performBlock {
do {
guard let fruit = (backgroundMOC.objectWithID(fruitMetaID) as? FruitMetaData)?.fruit else {
throw //Error
}
print(fruit.name) // "Banana"
fruit.name = "Apple"
fruit.needsSynchronization() //Sets syncStatus to 1
CoreDataController.sharedInstance.saveManagedContext(backgroundMOC)
}
catch {
//handle error
}
}
}
UPDATE AGAIN
Maybe I'm not creating the contexts right. Enlighten me please!
/// The parent to all other NSManagedObjectContexts. Responsible for writting to the store.
lazy var writerManagedObjectContext: NSManagedObjectContext =
{
let managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
managedObjectContext.performBlockAndWait {
managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator
}
return managedObjectContext
}()
lazy var mainManagedObjectContext: NSManagedObjectContext =
{
let managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.performBlockAndWait {
managedObjectContext.parentContext = self.writerManagedObjectContext
}
return managedObjectContext
}()
/// The context associated with background syncing..
func newBackgroundManagedObjectContext() -> NSManagedObjectContext
{
let backgroundManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
backgroundManagedObjectContext.performBlockAndWait {
backgroundManagedObjectContext.parentContext = self.mainManagedObjectContext
}
return backgroundManagedObjectContext
}
Holding onto child MOCs (children of the main context) is fraught with issues. I would recommend creating a new child (aka backgroundMOC) for each operation that you do.
Without seeing all of your code this looks like an issue with the child context getting out of sync.
Update
Assuming that your creation of the backgroundMOC sets the mainMOC as its parent then I wonder about the -objectWithID: and what it is returning.
I also wonder about your -performBlock: calls. In my head the threading looks fine but better to test. Try changing to -performBlockAndWait: just to test and see if there is a threading race condition. Not a permanent change but eliminates that part of the code as a source of the issue.
Before fetchRequest is called, you should reset context.
backgroundMOC.reset() // add this line
let fetchRequest = NSFetchRequest(entityName: entity.name)
let syncPredicate = NSPredicate(format: "%K == %d", JSONKey.SyncStatus.rawValue, 1)
fetchRequest.predicate = syncPredicate
return try backgroundMOC.executeFetchRequest(fetchRequest) as? [SyncableManagedObject] ?? []
The reason is FruitMetaData is an object(or class) so changing one of its properties/Core Data attributes does not register as a change to the results array ... the object references in the array remain the same.
And NSFetchRequest still returns the same result(by using cache). When use context.reset().This tells the context in the extension to fetch new data every time and ignore the cache.

Resources