How to correctly add additional section with FetchResultController? - ios

For now I'm sorting all my sections with the next sort descriptors:
let sortDescriptors = [NSSortDescriptor(key: "category", ascending: true), NSSortDescriptor(key: "name", ascending: true)]
let request = DBOShopCardBox.createFetchRequest(predicate: predicate, sortDescriptors: sortDescriptors)
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: DBContext.defaultContext, sectionNameKeyPath: "categoryOrderIndex", cacheName: nil) as? NSFetchedResultsController<NSManagedObject>
But now I want to the next:
I have an additional column, type. It has 3 values box, folder, free. I want to add one more(+1) section with ONLY records where type ==folder` in it and sort them alphabetically.
I was trying to add one more sort descriptor into my array like:
NSSortDescriptor(key: "type", ascending: true)
but it will sort ALL of them by type. But I need to get the records with type == folder, put them into the separate section and just later sort them. I'm confused now. Maybe someone can give me some hints or help me?
Thanks in advance!

That's not what NSFetchedResultsController does. It uses the result of a fetch (hence the name) to control (hence the name) the table view by serving as its model object. If you want the table view to express something the fetched results controller doesn't express (a different number of sections, etc.), you'll have to implement that yourself, probably as part of the data source methods (or you might subclass NSFetchedResultsController; I'm not sure about that part though). You can consult the fetched results controller's fetchedObjects to see what it fetched, but you will have to dictate explicitly that there is an extra section and what goes in it.

NSFetchedResultsController is a really cool class. It fetches from core data and updates the viewController when stuff is added, deleted, changed, or moved. It assigns indexPaths for every object it is monitoring so it can interface easily with a collectionView or a tableView. But you don't need to have a 1-to-1 relationship between a fetchedResultsController and a collectionView. You can have two fetchedResultsControllers - one for sections 0-3 and a different one for section 4.
The big challenge with this setup is to keep track of what kinds of indexPaths you are dealing with; the fetchedResultsControllers' indexPaths are no longer a one-to-one relation to the collectionView's. Make a few methods to convert from different indexPath spaces and back and make sure to call them correctly whenever you see an indexPath.

Related

How to put all related self-related objects in a tableview with associated section header for each relationship mapping?

I have a entity called "Skill". Skill has relationship mappings to itself for three different types - easier, harder, and similar.
I want to create a tableview using with 3 sections - easier, harder, and similar for a particular skill that I happen to be viewing. I am trying to determine how to create a fetch request using NSFetchedResultsController to collect all 3 of these skills into one. Here is my attempt
fileprivate lazy var relatedSkillsFetchedResultsController: NSFetchedResultsController<Skill> = {
let appDelegate =
UIApplication.shared.delegate as? AppDelegate
let managedContext =
appDelegate?.persistentContainer.viewContext
let request: NSFetchRequest<Skill> = NSFetchRequest(entityName: "Skill")
request.predicate = NSPredicate(format: "%# IN self.easier OR %# IN self.harder OR %# IN self.similar", skill!,skill!,skill!)
let skillFetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: managedContext!, sectionNameKeyPath: #keyPath(skill.relationshipFetchName), cacheName: nil)
return skillFetchedResultsController
}()
I was thinking - since easier is the inverse of harder and harder is the inverse of easier and similar is the inverse of similar.... I can fetch the original separately elsehow... then fetch all skills with the predicate that the main skill is in those skills harder or easier or similar.
Does this sound like the right way to do this?
I also don't know how to set the sectionNameKeyPath. Its basically would correspond to which of the three NSPredicate OR's that it matches right?
The difficulty with configuring a FRC is not so much finding the sectionNameKeyPath, though that can be difficult, but rather in getting a sort descriptor that is consistent with the desired sections: the sort descriptors compatible with a fetch request are relatively limited.
In your case, I would be tempted to create only a single, reflexive relationship from Skill to itself (relatedSkills), but to add an integer attribute to the Skill entity, to represent difficulty. You can decide on the scale (eg. 0-9, 0-99, whatever) and decide how close the difficulty must be (eg. +/-1, or +/-2, etc) for two skills to be judged "similar".
You can then use a predicate to select only those skills related to your chosen Skill:
NSPredicate(format:"ANY relatedSkills == %#", chosenSkill)
and add a sort descriptor based on difficulty. Then define a function to determine (using the rules you decide upon) whether any given Skill is "easier", "similar", or "harder". That function will be your sectionNameKeyPath.
Update
Well, having pondered this overnight, I realise that I was wrong. In the above scenario, the sectionNameKeyPath is the difficult bit: the problem is that it depends on the currently selected Skill, but the function can take no parameters, so cannot “know” which is the currently selected skill. Back to the drawing board....

NSFetchRequest fetchBatchSize not working

I have been having problems with fetchBatchSize in an NSFetchedResultsController so I decided to take a step back and use it on just a plain NSFetchRequest. I created a very simple project with only a tableViewController that has 100 items. The items have only two properties, itemID and itemName. I use the following method to populate the data array:
func initializeItems() {
let request = NSFetchRequest<Item>(entityName: "Item")
let messageSort = NSSortDescriptor(key: "itemName", ascending: true)
request.sortDescriptors = [messageSort]
request.fetchBatchSize = 20
do {
let fetchedResults = try context.fetch(request)
if fetchedResults.count > 0 {
self.items = fetchedResults
//self.tableView.reloadData()
}
} catch {
print("Could not fetch \(error)")
}
}
This is called in viewDidLoad of the tableViewController. The context is defined as follows:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
self.context = appDelegate.persistentContainer.viewContext
When I run the app, the table shows fully populated (as indicated by the scroll bar) with all rows.
I don't understand why the fetchBatchSize is being ignored.
Documentation is as follows:
The batch size of the objects specified in the fetch request.
The default value is 0. A batch size of 0 is treated as infinite, which disables the batch faulting behavior.
If you set a nonzero batch size, the collection of objects returned when an instance of NSFetchRequest is executed is broken into batches. When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but only data for objects up to the batchSize will be fetched from the persistent store at a time. The array returned from executing the request is a proxy object that transparently faults batches on demand. (In database terms, this is an in-memory cursor.)
You can use this feature to restrict the working set of data in your application. In combination with fetchLimit, you can create a subrange of an arbitrary result set.
I can see the 100 items in the fault array being loaded. But then it loops through 5 loads of 20 items full data prior to scrolling the tableView. The tableView shows about 15 rows at a time.
The behaviour I want is for the data to be loaded in 20 item batches as the table is scrolled
As shown in the documentation:
The fetch limit specifies the maximum number of objects that a request
should return when executed.
fetchLimit is what you're looking for.
I suspect something in your code actually iterates through that array and causes it to fire the fault. I'm not telling you do it deliberately yourself - it is quite likely that setup od your UITableView does that (pre-calculating cell height based on content, sectioning data, etc).
What I suggest is to open Core Data profiling tool in Instruments and check what is in the stack trace when faults are fired.
fetchBatchSize works best with NSFetchedResultsController. Put ur fetchRequest in NSFetchedResultsController and then use NSFetchedResultsController to access fetched result. Then definitely fetchBatchSize gonna work. Or u can google how NSFetchedResultsController is implemented in fetching result.

Sort Order vs Display Sort Order

I have an app that displays a section title followed by the detail items followed by the next section and its details. Everything works fine, but I would like to rearrange the order in which the sections are shown. The problem is that I need to order the core data by the report-id then status then the date to get the correct detail items to show under the proper section.
let sortDescriptor1 = NSSortDescriptor(key: #keyPath(Item.report.id), ascending:true)
let sortDescriptor2 = NSSortDescriptor(key: #keyPath(Item.report.status), ascending:true)
let sortDescriptor3 = NSSortDescriptor(key: #keyPath(Item.report.dateStarted), ascending:false)
let sortDescriptor4 = NSSortDescriptor(key: #keyPath(Item.date), ascending:true)
How can I change the sort/display order while still maintaining the proper relationship between the section (report) and the detail items associated with it? The report.id is a UUID so currently the reports end up in random order.
A fetchedResultsController has a property sectionNameKeyPath which can be used to group items togethers. This only works if the sectionNameKeyPath groups the items in the same order that they are sorted. For example: you can sort by date and then group by hour or week or any other time based grouping, but not by name. In your case you want the sections to be sorted in a way that does not not match how the items are grouped. There may be some clever solution for your particular situation, but since you didn't give any details I can only give a general solution.
The indexPath that is returned from a fetchedResultsController is really useful for interoperability with a tableView or collectionView. But it does not have to be a one-to-one relationship. For example you could have a setup where one section points to one fetchedResultsController and another section points to a different one. The key in doing this setup is to make sure to not confuse the fetchedResultsController indexPath with your collectionView indexPath. Generally I find having a separate object to manage converting the indexPath the easily way to keep it straight.
Create a separate object that sorts the sections after the fetchedResultsController does a fetch (and after a section is inserted or deleted). Inside the indexPathsManager have a dictionary the maps between the "local" indexPath and the fetchedResultsController indexPath. Make sure to sure use this object EVERY TIME you use indexPaths in the viewController. Also make sure to convert the indexPaths when you update the view after the fetchedResultsController delegates that there is a change. And updating the indexPathsManager when any sections are inserted or deleted.
TL;DR Sort the sections of the fetchedResultsController after the fetch and convert your tableView's indexPath to fetchedResultsController indexPaths.

Creating sections with NSFetchedResultsController, on the fly

I'm using NSFetchedResultsController (NSFRC) to display information in a UITableView. I'm trying to create the option for the user to sort the cells in sections as opposed to alphabetically. The problem is, the sections would then be determined using downloaded information. On top of this the section for each item will be changing relatively often so I don't want to save the section. I have noticed the mention of transient attributes, in my research of similar problems, but i've never used these before I'm not sure if I can use them baring in mind that all the calculations are done once the data has already been loaded, and I also want this solution to be compatible with my previous Core Data database. Also I'm not particularly great at Core Data, (nor Objective-C at that!) so I'm not entirely sure how I'd go about doing this.
So here's what I want to go for if we're using transient attributes (this next bit is theoretical as I don't know if transient attributes are the correct way forward). I would like 4 possible sections, 0-3 (I'll rename them using the TableView delegate to get around sorting problems). When the calculations are done, each cell will be assigned the transient attribute (if needed, the default section would be 2). I hope this all makes sense.
Right, now for some theoretical code. First I create the transient property in the Data Model screen-thing, and make it transient by checking the transient check box... Sounds simple enough.
In the code for the calculations in willDisplayCell (needs to be done in wDC for a couple of reasons), the entity could be saved like this:
MyEntity *myEntity = [self.fetchedResultsController objectAtIndexPath:indexPath];
myEntity.sectionTransientProperty = 2;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error: %#", error);
FATAL_CORE_DATA_ERROR(error);
return;
}
Done, right? Is that how we assign a value to a transient property?
Then I change the sorting option in NSFRC when I alloc it:
fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"sectionTransientProperty"
cacheName:#"MyEntity"];
How are we doing, what else do I need to do? Or have I got this so horribly wrong I should just give up on Core Data and NSFRC? If you guys could help guide me through this I'd really appreciate it. If you need me to post any more code I would be happy to.
Regards,
Mike
If you want an FRC with sections, you have to add a sort descriptor to the fetch request, and that sort descriptor cannot be based on transient attributes.
See the documentation of initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:`:
If the controller generates sections, the first sort descriptor in
the array is used to group the objects into sections; its key must
either be the same as sectionNameKeyPath or the relative ordering
using its key must match that using sectionNameKeyPath.
and Fetch Predicates and Sort Descriptors in the "Core Data Programming Guide":
The SQL store, on the other hand, compiles the predicate and sort
descriptors to SQL and evaluates the result in the database itself.
This is done primarily for performance, but it means that evaluation
happens in a non-Cocoa environment, and so sort descriptors (or
predicates) that rely on Cocoa cannot work. The supported sort
selectors are ...
In addition you cannot sort on transient properties using the SQLite store.
This means that you cannot create sections purely on transient attributes. You need a persistent attribute that creates the ordering for the sections.
UPDATE: A typical use of a transient attribute as sectionNameKeyPath is: Your objects have a "timeStamp" attribute, and you want to group the objects into sections with one section per month (see the DateSectionTitles sample code from the iOS Developer Library). In this case you have
a persistent attribute "timeStamp",
use "timeStamp" as first sort descriptor for the fetch request,
a transient attribute "sectionIdentifier" which is used as sectionNameKeyPath. "sectionIdentifier" is calculated from "timeStamp" and returns a string representing the year and the month of the timestamp, e.g. "2013-01".
The first thing the FRC does is to sort all fetched objects according to the "timeStamp" attribute. Then the objects are grouped into sections according to the "sectionIdentifier" attribute.
So for a FRC to group the objects into sections you really need a persistent attribute. The easiest solution would be to add a persistent attribute "sectionNumber" to your entity, and use that for "sectionNameKeyPath" and for the first sort descriptor.

NSFetchedResultsController with multiple section logic

I have a list of items that are added to a shopping list. Each item has a department, and a flag to say whether it has been collected or not.
I would love to use a single table view to display this. The problem is, I need to group the sections by department, and then add a final additional section at the bottom for collected items (regardless of departments).
I can create a fetchedResultsController filtering out collected = yes and sorting by department easily enough. But I need this additional section appended with those items where collected = yes.
Should I be trying to create a fetchedResultsController with a sophisticated query to do this? Or try to manually add a section and some rows? If so, from where? A second FRC? Is it possible to have two FRCs feed into a single tableview?
Ben,
The NSFetchedResultsController is designed to provide a single fetch to fill a table view. But you have the choice of how that data is fed into the table view. You can manually translate the fetched items where ever you want. You can add sections, extra rows, whatever. That is why they surface the update of the model in the delegate. They give you the chance to make those adjustments.
Hence, if you construct your model such that a single fetch does the job, then you have a great deal of flexibility on how you use it.
Andrew
sectionKeypathName is available which takes a parameter to decide how many sections should be there. If you pass nil, there will be only one section but if you pass some sectionKeypathName in FetcherResultsController and set the first sortDescriptor same as the sectionKeyPathName, you will get different sections in fethedResultsControllerDelegate. That sectionKeypathName must be attribute in an entity you are fetching.
let sortDescriptor = NSSortDescriptor(key: "sectionView", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: mainContext, sectionNameKeyPath: "sectionView", cacheName: nil)

Resources