How to trigger an section update in NSFetchedResultsController - ios

I am using NSFetchedResultsController to display a list of workouts grouped in sections with the date on which they were completed.
Graphically it can be represented as this:
JAN 20, 2020
Workout 1
Workout 2
JAN 21, 2020
Workout 1
Workout 2
Each section display's the total distance covered by the user on that day.
Whenever I update any workout (change it's distance) the tableView cell's are updated but the section which contains the total distance for day doesn't change.
I have to reload the tableView to get the changes in effect.
The delegate method in NSFetchedResultsControllerDelegate : controller(_:didChange:atSectionIndex:for:) is only called when a new section is added or removed.
I have searched many SO questions but haven't found any solution. Would be great if any one pointed me in the right direction regarding this.
Below is how I am creating my NSFetchResultsController
lazy var fetchedResultsController: NSFetchedResultsController<CD_Run> = {
let fetchRequest = NSFetchRequest<MY_ENTITY>(entityName: "MY_ENTITY")
let sectionSortDescriptor = NSSortDescriptor(key: "daySectionIdentifier", ascending: false)
let startTimeSortDescriptor = NSSortDescriptor(key: "startTime", ascending: false)
fetchRequest.sortDescriptors = [sectionSortDescriptor, startTimeSortDescriptor]
fetchRequest.fetchBatchSize = 200
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: CoreDataManager.shared()._managedObjectContext,
sectionNameKeyPath: "daySectionIdentifier",
cacheName: nil)
controller.delegate = self
return controller
}()

Related

Swift NSFetchedResultsController not sectioning based on sectionNameKeyPath

lazy var fetchedResultsController: NSFetchedResultsController<StoryTemplatePack> =
{
let managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).managedObjectContext
var frc: NSFetchedResultsController<StoryTemplatePack> = NSFetchedResultsController(fetchRequest: self.createFetchRequest(), managedObjectContext: managedObjectContext, sectionNameKeyPath: "sectionnum", cacheName: nil)
frc.delegate = self
return frc
}()
// FetchedResultsController: Create Request
func createFetchRequest() -> NSFetchRequest<StoryTemplatePack> {
let fetchRequest = NSFetchRequest<StoryTemplatePack>(entityName: "StoryTemplatePack")
fetchRequest.predicate = NSPredicate.init(format: "productidentifier != nil")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "isfeatured", ascending: false),
NSSortDescriptor(key: "ispurchased", ascending: true),
NSSortDescriptor(key: "name", ascending: true)
]
return fetchRequest
}
The results should be sectioned according to the sectionnum property on StoryTemplatePack (which is a transient property calculated in code).
When I loop over the results of the NSFetchedResultsController it shows the correct section numbers and story pack names. There should be 3 sections, but the FRC is reporting only 1 section. The dumped results also show that I'm sorting the results correctly so that the section values are monotonically increasing.
---- dumping frc, num sections: 1
section 0 pack Incredible Journeys
section 0 pack Out for a Date
section 0 pack Party Time
section 1 pack Holiday Craziness
section 1 pack In the Office
section 1 pack On the Internets
section 1 pack On the Job
section 1 pack School Daze
section 1 pack Social Networking
section 1 pack TV Troubles
section 1 pack Technology Takes Over
section 2 pack Christmas Shenanigans
---- done dumping
self.fetchedResultsController.sections!.count is returning 1 when it should return 3.
What am I doing wrong or missing?

NSFetchResultController loading all messages at once, fetchbatchsize is not fixing it

I am at the stage of optimizing my core data code,
I am making a messaging app, and I only want to load 50 messages at a time.
However, looking at the timeprofile Data below, the fetchrequest loads all the messages at once. Afterwards, it loads all the messages in batches of 50 without even scrolling. So it's like not even doing anything.
Any help would be greatly appreciated.
lazy var fetchedResultsControler: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Mesages")
fetchRequest.fetchBatchSize = 20
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "user.id = %#", self.friend!.id!)
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
override func viewDidLoad() {
super.viewDidLoad()
do {
try fetchedResultsControler.performFetch()
} catch let err {
print(err)
}
self.fetchData = fetchedResultsControler.fetchedObjects as! [Mesages]}
This is the CoreData time profiling data:
That's correct behavior, per the NSFetchRequest documentation:
When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but no more than batchSize objects’ data will be fetched from the persistent store at a time. The array returned from executing the request will be a proxy object that transparently faults batches on demand. (In database terms, this is an in-memory cursor.
If, instead, you want explicit control of the fetch grouping, use a combination of fetchLimit and fetchBatchSize instead. But at that point you're fighting the features of NSFetchedResultsController.
What are you really trying to do with the fetchData array?

How to get the latest objects from NSFetchedResultsController with a limit?

Maybe the title of question is not apriopriate, but here is what I need to achieve with my NSFetchedResultsController:
private func setupOnceFetchedResultsController() {
if fetchedResultsController == nil {
let context = NSManagedObjectContext.MR_defaultContext()
let fetchReguest = NSFetchRequest(entityName: "WLComment")
let createdAtDescriptor = NSSortDescriptor(key: "createdAt", ascending: true)
fetchReguest.predicate = NSPredicate(format: "item.identifier = %d", item.identifier)
fetchReguest.sortDescriptors = [createdAtDescriptor]
fetchReguest.fetchLimit = 10
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchReguest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
try! fetchedResultsController.performFetch()
tableView.reloadData()
}
}
Suppose I have about 100 comments. The oldest on the top, and the latest on the bottom. There is no problem when I need to display all of them, but here I need only 10 of them. The above NSFetchedResultsController will display the first 10 of 100 messages, but I need to display the latest 10.
Is there a way to let it know what I need?
Depending on how you are going to use the data later on, you can choose from at least two options:
revert the sort descriptor, will return the end of the sorted list with reverted order;
get the number of items first and set the appropriate fetchOffset, will return the required number of items, ordered as needed.

CoreData FetchRequest problems

I'm teaching myself to programme and have thought up this project for myself to learn. I'm having trouble with my code, I was able to save it correctly and load the first state population TVC. However, I'm having problems with the state and number of animals per state TVC. I want to total it per a state. So I would be adding the dogs and cats population together and get the total per a state, but it brings Alabama separately with two different population, can someone help me with this please.
the model below shows how I want it, I'm able to output to State Population correctly but now the second one.
What my code is doing for the second one is that it's getting the data from coredata but I'm using sort descriptor because I don't know any other way to pull the data.
var totalEntries : Int = 0
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc : NSFetchedResultsController = NSFetchedResultsController()
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Animals")
let sortDescriptor = NSSortDescriptor(key: "state", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
func getFRC() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
return frc
}
override func viewDidLoad() {
super.viewDidLoad()
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to fetch data")
return
}
totalEntries = moc.countForFetchRequest(fetchRequest(), error: nil) as Int!
self.tableView.reloadData()
}
override func viewDidAppear(animated: Bool) {
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to fetch data")
return
}
totalEntries = moc.countForFetchRequest(fetchRequest(), error: nil) as Int!
self.tableView.reloadData()
}
Your problem is the fetched results controllers aren't designed to show aggregated fetch results like you desire, hence you see all the underlying data instead of the aggregate.
You could use the FRC if you cheat... Set the section name of the FRC to the state name, then you will have one section in the table per state. In the table delegate return 1 for the number of rows in each section. When configuring the cell use KVC to #sum the populations of all of the animals in that state (the rows for that section as understood by the FRC).
This is a memory and runtime inefficient solution... It could be improved by caching the calculated sums, but you're adding logic on top of bad design then.
The correct approach would be to abandon the FRC and use a simple array of dictionaries. This would be generated by changing your fetch request to return dictionary type and configuring it to calculate the sums for you. That's done with an expression something like:
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
expressionDescription.name = #"sumOfPopulations";
expressionDescription.expression = [NSExpression expressionForKeyPath:#"#sum.population"];
expressionDescription.expressionResultType = NSDecimalAttributeType;

TableView section update depend on CoreData transient property change

I'm trying to develop a timetable app.
I have TableViewController which shows the classes on current day. In viewDidLoad( ) I fetch the classes with NSFetchedResultsController:
let fetchRequest = NSFetchRequest(entityName: "Lessons")
let predicate = NSPredicate(format: "startsday = '\(dateFormatter.stringFromDate(NSDate()))'", NSDate())
fetchRequest.predicate = predicate
let sortDescriptorStarts = NSSortDescriptor(key: "starts", ascending: true)
let sortDescriptorTitle = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptorStarts, sortDescriptorTitle]
fetchRequest.fetchBatchSize = 30
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
fetchedResultsController.delegate = self
fetchedResultsController.performFetch(nil)
And when I set sectionNameKeyPath: "sectionIdentifier"(which is a transient property) they are sorted into three groups "Past", "Now", "Next" depend on current time. It is working. I have a screenshot here.
My problem is: As time goes by the sections aren't updated
The rows should move out and move in other sections and eventually they should all go into the "Past" section, and the "Now", "Next" sections should be deleted.
I can update the sectionIdentifier transient property, but my fetchedResultController doesn't want to notice the changes...
I don't want to use tableView.reloadData( ) because i want animations.
(There is a similar question, but the end of it is different. I can't get my answer.)
Thanks!
I have found a solution for this problem. It is definitely not the best but it's working nicely. (Swift 2.3)
First, I have a transient property called "sectionIdentifier".This could be "NOW", "NEXT" and "PAST" depend on the current time.I added a new persistent property next to this transient called "sectionID". This property's purpose is to tell if the lesson status (NOW, NEXT, PAST) changed. I changed my original fetch to this:
let fetchRequest = NSFetchRequest(entityName: "Lessons")
let predicate = NSPredicate(format: "startsday = '\(dateFormatter.stringFromDate(NSDate()))'", NSDate())
fetchRequest.predicate = predicate
let sortDescriptorStarts = NSSortDescriptor(key: "starts", ascending: true)
let sortDescriptorTitle = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptorStarts, sortDescriptorTitle]
fetchRequest.fetchBatchSize = 30
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch _ {
//Handle error
}
if let lessons = fetchedResultsController.fetchedObjects as? [Lessons] {
for lesson in lessons {
if lesson.sectionID != lesson.sectionIdentifier! {
lesson.setValue(lesson.sectionIdentifier!, forKey: "sectionID")
moc.refreshObject(lesson, mergeChanges: true)
}
}
}
After the fetch I set this persistent property to the transient's value. (last 8 line above)
In viewDidLoad() I added this line.
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(TodayViewController.update), userInfo: nil, repeats: true)
I update my transient property every second.
My update method looks like this:
func update() {
for lesson in fetchedResultsController.fetchedObjects as! [Lessons] {
if lesson.sectionID != lesson.sectionIdentifier! {
lesson.setValue(lesson.sectionIdentifier!, forKey: "sectionID")
moc.refreshObject(lesson, mergeChanges: true)
do {
try moc.save()
}
catch _ {
//Handle error
}
}
}
}
So my transient property will always give the lessons current status and the persistent one will alway give the tableviewcell's status. If there is difference I put my transient value into the persistent property and with moc.refreshobject() i will get a the proper FetchedResultControllerDelegate method and i can handle the tableview changes there.

Resources