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)
Related
Large data sets have been stored in the Core data local storage. I have search functionality in my app. Based on user search, a list is generated. Users can select items from the list. After selection, a new list is generated & displayed in another page. Newly generated list will contain object that the user selected & some other objects that are just before & after of the selected objects. Every object has time property & time property should be used for ordering the objects. How can i fetch efficiently before & after objects for a particular selected/target object. In the local storage, there could be some data which might have same time interval.
Any help is appreciated.
I don't have a single answer for this since mainly it depends which gives the best performance
The first one is the most precise one but it requires two fetch request and a fetch request is costly:
Simply create two fetch requests to fetch 10 before and 10 after,
Before
request.fetchLimit = 11
NSPredicate(format: "%K <= %#", #keyPath(timestamp), selectedTimestamp)
NSSortDescriptor(keyPath: \SomeEntity.timestamp, ascending: false)
and after
request.fetchLimit = 10
NSPredicate(format: "%K > %#", #keyPath(timestamp), selectedTimestamp)
NSSortDescriptor(keyPath: \SomeEntity.timestamp, ascending: true)
This will give you 10 before the selected object (selectedTimestamp) and 10 after, notice that = is used in only one predicate to not include duplicates. You can of course swap the place of the equal sign. I did set the firs fetch limit to 11 since it will include the selected object but a better solution is to create a compound predicate where you exclude the already selected object.
If you have a lot of objects with duplicate timestamps it will be random which 10 gets returned
The other solution could be used if the timestamps of the objects are roughly evenly distributed because then you could fetch based on a time interval and setting the fetch limit to a higher but still relatively small number like 100 and then extract two arrays from the result set in a simple manner based on the selected timestamp.
This way you only need one fetch request and the rest of the work should be fast
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....
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.
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.
I'm banging my head trying to figure out something in Core Data that I think should be simple to do, and I need some help.
I have a data store which contains data from the past two years, but in my app, I have certain criteria so that the user only works with a subset of that data (i.e. just the past month). I have created the predicates to generate the fetch request and all that works fine.
My issue is that I then want to run some additional predicates on this subset of data (i.e. I just want objects with name=Sally). I'd like to do so without having to re-run the original predicate with an additional predicate (in a NSCompoundPredicate); I'd rather just run it on the subset of data already created.
Can I just run a predicate on the fetch results?
Is the format of the predicate the same as for the initial calls into the core data store?
Thanks for any help.
You could filter the original array of results using a predicate. See the NSArray filteredArrayUsingPredicate method.
The only reason I would consider doing what you're talking about is if you find that modifying the predicate at the FetchedResultsController level caused significant performance degradation.
My recommendation is to get a reference to your fetchedResultsController in your view controller and update your predicate with a compound predicate matching your additional search parameters.
If you were to tie your viewControllers data source to a predicate of a predicate you wouldn't be able to properly utilize the NSFetchedResultsControllers Delegate methods that allow for easy, dynamic updating of Views such as table view and collection view.
/* assuming you're abstracting your datastore with a singleton */
self.fetchedResultsController = [DataStore sharedStore].fetchedResultsController;
self.fetchedResultsController.delegate = self;
[self.fetchedResultsController.fetchRequest setPredicate:yourPredicate];
Make sure to configure your fetch requests' batch size to a reasonable value to improve performance.