fetchBatchSize and cache in NSFetchedResultsController ignored - ios

I am using an NSFetchedResultsController to display messages in a chat room app.
The context variable is assigned in appDelegate and a reference to that context used in the chat room.
let context = persistentContainer.viewContext
I initialize the NSFRC as follows in viewDidLoad:
func initializeResultsController() {
let request = NSFetchRequest<Message>(entityName: "Message")
let messageSort = NSSortDescriptor(key: "dateCreated", ascending: true)
request.sortDescriptors = [messageSort]
request.predicate = NSPredicate(format: "chatRoomId == %#", self.chatRoomId)
request.fetchBatchSize = 30
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "messageDateSectionIdentifier", cacheName: self.chatRoomId)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}
The sectionNameKeyPath ("messageDateSectionIdentifier") is a derived property so that the sections can be divided into calendar days.
I have two problems. Firstly the batchSize seems to be ignored and secondly the cache seems to make no difference to the performance. The more messages the longer the delay when selecting the chat room. about 1 second for 1500 messages.
When I edit scheme to show coreData info in console, the batch request for 30 rows is performed multiple times when the view first appears and in one case the array size is 1500. Not sure whether that is the fault array or the populated array. The console printOut is:
CoreData: annotation: sql connection fetch time: 0.0013s
CoreData: annotation: total fetch execution time: 0.0014s for 1454 rows.
CoreData: annotation: Bound intarray _Z_intarray0
CoreData: annotation: Bound intarray values.
And this is repeated after this multiple times with value of 30 rows.
I have tried simplifying the sectionNameKeyPath to just dateCreated to see if the derived sections were the problem but there was no difference at all. I should also mention that as with all chat apps, the app initially scrolls to the bottom when it is presented.
What I want is for the cache to work and also for the fetchBatchSize to work so that only 30 rows are fetched from coreData initially until the user starts to scroll up. The delay now caused by this method is having a measurable impact on my app performance.

You are correct that batchSize is not respected by a fetchedResultsController. A NSFetchedResultsController does a fetch and then tracks all changes in the context to see if anything is added, removed, moved or changed. If it only fetched a subset of the matching entities by respecting batchSize it would be unable to do it's job.
You can get around this by setting a predicate to only fetch message after a certain date. In order to figure out what is the cutoff date you can do a single fetch first, where batchSize = 1 and the batchOffset = [how many message you want initially in you fetchedResultsController]. As more message come in the collection will increase in size beyond your initial limit.
Also be aware that the sectionNameKeyPath is called for EVERY element in the collection. So doing even a small amount of work there can cause huge delays. Don't create a calendar or a dataFormatter in then sectionNameKeyPath - reuse a single one.

I finally found the cause of the problem in my case.
If you are referencing the fetchedResultsController in tableView heightForRowAt then the fetchBatchSize will loop through and load all data in loops of the fetchBatchSize you specify.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let item = self.fetchedResultsController!.object(at: indexPath)
// get and return height of item
return item.heightOfItem
}
If you use UITableViewAutomaticDimension or define a height that doesn't require a reference to fetchedResultsController (i.e. a fixed height) then you won't have this problem and fetchBatchSize will work as it should.
Unfortunately I found UITableViewAutomaticDimension not acceptable for scrolling performance so I guess I have to manually configure batch loads using offsets.
I also had one additional issue that was causing the loop loads of data. That was the sectionNameKeyPath was in my case a transient property. This also caused problems but unfortunately is necessary.
If you are having problems with fetchBatchSize with an NSFetchedResultsController I would advise looking at these two issues.

Related

How does NSFetchedResultsController know what selected range of row to read from CoreData instead of reading entire all rows?

My understanding on NSFetchedResultsController is that, it will not load entire all rows from CoreData at once.
Based on the current visible UI table row on screen, NSFetchedResultsController will just load a small amount of rows, just to be good enough to be shown on screen.
But, when I try to read and test the official code from Apple, this is contrast with my perception.
https://github.com/yccheok/earthquakes-WWDC20/blob/master/LoadingAndDisplayingALargeDataFeed/Earthquakes_iOS/QuakesViewController.swift#L123
There are around 10,000++ items in the CoreData database.
When the app is started for the 1st time, UITableView is not being scrolled yet, and the following function being executed for the 1st time
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var count = dataProvider.fetchedResultsController.fetchedObjects?.count ?? 0
print(">>> SIZE \(count)")
// Try to inspect the content...
var quake = dataProvider.fetchedResultsController.fetchedObjects?[count-1]
print("----> \(quake?.place)")
return count
}
When setting up NSFetchedResultController, I also try to change the batch size explicitly to 1, just to see what is the outcome - https://github.com/yccheok/earthquakes-WWDC20/blob/master/LoadingAndDisplayingALargeDataFeed/Shared/QuakesProvider.swift#L284
/**
A fetched results controller to fetch Quake records sorted by time.
*/
lazy var fetchedResultsController: NSFetchedResultsController<Quake> = {
// Create a fetch request for the Quake entity sorted by time.
let fetchRequest = NSFetchRequest<Quake>(entityName: "Quake")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
fetchRequest.propertiesToFetch = ["magnitude", "place", "time"]
// Just for testing purpose to avoid NSFetchedResultsController
// load all data at once into memory.
fetchRequest.fetchBatchSize = 1
// Create a fetched results controller and set its fetch request, context, and delegate.
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: persistentContainer.viewContext,
sectionNameKeyPath: nil, cacheName: nil)
controller.delegate = fetchedResultsControllerDelegate
// Perform the fetch.
do {
try controller.performFetch()
} catch {
fatalError("Unresolved error \(error)")
}
return controller
}()
When app launched for first time, the following outcome is printed
>>> SIZE 10902
----> Optional("43 km E of Teller, Alaska")
My questions are
Currently, there are only less than 20 visible UI table row shown on screen. But, it seems like CoreData has already load all 10,000++ rows at once? Is this an expected behaviour for NSFetchedResultsController. As, my understanding is having NSFetchedResultsController, is to avoid having to load all unnecessary data into memory in 1 time.
I do not see a way, on how UITableView communicate back to NSFetchedResultsController regarding the visible UI row range. Isn't UITableView suppose to tell NSFetchedResultsController that "Currently, UI row 0th till 14th are visible on screen. Please only load data in range 0th till 14th from CoreData database"?
Thanks.
You are mistaken in your understanding of NSFetchedResultsController and its purpose. It makes it easier to use Core Data with table views, particularly where objects are inserted, deleted or updated; You can use delegate methods to efficiently handle these actions without needing to reload the entire table view.
That said, there is a default behaviour of Core Data that means, in effect, only visible rows are loaded into memory regardless of whether you use an NSFetchedResultsController or not.
The default behaviour of Core Data is to only provide fault objects at first; A fault is a lightweight representation of an object. When a property of the object is accessed Core Data fetches the full object from the persistent store and loads it into memory.
This may have the effect of only loading full objects for on-screen rows since cellForRow(at:) is only called for visible rows (plus a few extra to allow for scrolling). If the only access to your fetched objects is in that function, then the faults will only fire for those rows.
As more rows become visible, cells will be required and more faults will fire, loading more data.
You can disable this faulting behaviour but it will increase memory use.
You can use UItableViewDataSourcePrefetching to fire faults ahead of display, but in many cases you won't see a difference in the UI.

How to build subsections in an NSFetchedResultsController

I'm building an expense tracker where an Expense can belong to only one Category but can have multiple Tags. This is my object graph:
In the screen where I list all the expenses in a table view, I want the expenses to be grouped by date (the sectionDate), and then by Category (or, using a segmented control, by Tag). This is the intended UI:
I can already make an NSFetchedResultsController query all expenses, section them by date, then by category, but I can't get the (1) total for the category and (2) the list of expenses in it. How might I proceed to do that? This is my current code:
let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> = {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Expense")
fetchRequest.resultType = .dictionaryResultType
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: #keyPath(Expense.sectionDate), ascending: false)
]
fetchRequest.propertiesToFetch = [
#keyPath(Expense.sectionDate),
#keyPath(Expense.category)
]
fetchRequest.propertiesToGroupBy = [
#keyPath(Expense.sectionDate),
#keyPath(Expense.category)
]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: Global.coreDataStack.viewContext,
sectionNameKeyPath: #keyPath(Expense.sectionDate),
cacheName: nil)
return fetchedResultsController
}()
A should forewarn that I've never done this, but personally I would set about it as follows:
Don't use propertiesToGroupBy: it forces you to use the .dictionaryResultType which means you can only access the underlying managed objects by executing a separate fetch.
Instead, add another computed property to the relevant NSManagedObject subclass, combining the sectionDate and the category.name. This property will be used as the sectionNameKeyPath for the FRC, so that the FRC will establish a section in the tableView for each unique combination of sectionDate and category name.
Add category.name as another sort descriptor for the fetch underlying the FRC. This will ensure that the Expense objects fetched by the FRC are in the correct order (ie. all Expense objects with the same sectionDate and category name are together).
Add a section header view for each section. The name property for the section (from the FRC) will include both the sectionDate and the category name. In most cases, you can strip out and ignore the sectionDate, displaying only the category name and corresponding total (see below). But for the very first section, and indeed the first section for any given sectionDate, add an additional view (to the section header view) showing the sectionDate and overall total for that sectionDate.
Working out whether a given section is the first section for the sectionDate is a little tricky. You could retrieve the name for the previous section and compare the sectionDates.
To collapse/expand the sections, maintain an array holding the collapsed/expanded state of each section. If the section is collapsed, return 0 in the tableView numberOfRowsInSection datasource method; if expanded use the figure provided by the FRC.
For the category totals, iterate through the objects array for the relevant section (or use a suitable .reduce to achieve the same).
For the sectionDate totals, filter the fetchedObjects for the FRC to include only the Expense objects for the relevant sectionDate, and then iterate or .reduce the filtered array.
I am happy to add or amend if any of that needs clarification.
I appreciate #pbasdf's answer, but I feel that I'll have a hard time wrapping my head around the solution after a long time of not looking at the code.
What I've come around to doing is instead of fetching Expense objects, I defined a new entity for the subsections themselves (CategoryGroup, and I will also make a TagGroup) and fetch those entities instead. These entities have references to the Expense objects that they contain, and the Category or the Tag that represents the group. This is my (partially complete) data model:
And my NSFetchedResultsController is now far simpler in code:
let fetchedResultsController: NSFetchedResultsController<CategoryGroup> = {
let fetchRequest = NSFetchRequest<CategoryGroup>(entityName: "CategoryGroup")
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: #keyPath(CategoryGroup.sectionDate), ascending: false)
]
return NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: Global.coreDataStack.viewContext,
sectionNameKeyPath: #keyPath(CategoryGroup.sectionDate),
cacheName: "CacheName")
}()
The downside is that I now have to write extra code to make absolutely sure that the relationships among the entities are correctly defined whenever an Expense or a Category is created/updated/deleted, but that's an acceptable tradeoff for me as it is easier to comprehend in code.

Changing NSFetchedResultsController's fetchRequest.predicate does not trigger delegate

I've got a NSPredicate on my FRC's fetchRequest. At some point an object is added to CoreData, I create a new predicate, update the fetchRequest and perform a fetch:
self.fetchedAddressesController.fetchRequest.predicate = self.predicate;
BOOL success = [self.fetchedAddressesController performFetch:nil];
This does however not invoke the FRC's delegate methods like controllerWillChangeContent:. And my table view is not updated.
However, when I add the following line:
[self.tableView reloadData];
below the two shown above, I do see the expected update. This shows that the data to be displayed has indeed changed.
I've checked the new predicate and it's fine. I also set FRC's delegate to self, and its methods are invoked in other cases.
Any ideas already what could be wrong?
The behaviour you have described is expected. According to the Apple documentation, if you want to modify a fetch request on a NSFetchedResultsController, you must delete the cache (if you are using one), modify the NSFetchRequest, then invoke performFetch: (which won't call any of the delegate methods).
If you want to know what has changed between the predicates, you need to store the old state and compare. A library that I've used in the past for this is Doppelganger.
The delegate methods are called when the FRC observes changes to the set of fetched objects it has eyes on after you do a fetch. If you go and change the predicate and do a new fetch, the FRC is reset and is now observing a different set of objects. The delegate methods aren't called because nothing changed in the original set of objects.
Careful if you are literally changing the entity...
We had a tricky one, not only the sort changed but the actual entity class changed, so for two different tab buttons,
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Cats")
but then ..
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Dogs")
In fact if you're doing that, THIS will NOT work:
func buttonLeft() {
mode = .cats
bringupFetchRequest()
pull.feedForCats() // get fresh info if any at the endpoints
}
func buttonRight() {
mode = .dogs
bringupFetchRequest()
pull.feedForDogs() // get fresh info if any at the endpoints
}
It will eventually crash.
It seems you need this:
func buttonLeft() {
mode = .cats
bringupFetchRequest()
tableView.reloadData()
pull.feedForCats() // get fresh info if any at the endpoints
}
func buttonRight() {
mode = .dogs
bringupFetchRequest()
tableView.reloadData()
pull.feedForDogs() // get fresh info if any at the endpoints
}
That (seems to) reliably work.

How do I avoid long loading time with custom cell in a UITableView when using a big database

In the app I'm working on, I have to fetch and use data (text and images) from a quite big database, and, when load the tableview, the app freezes for 2-5 seconds. This is probably going to ruin the UX as the UI doesn't feel quick to respond to an action.
I think that the problem is the presence of many images (not really heavy, 50x50px), which are one for each custom cel, and there are about 1000+ rows.
What I need is to know which is the best way to avoid this long loading time, or how to hide it.
The user opens the tableview clicking on a button, which perform a segue, and then I fetch data in viewDidLoad(). As I figured out, the viewDidLoad() method can block interaction until it's finished. Inside viewDidLoad() I call a function called fetchCard(). It puts in an array the Card object, which contains the properties I use in the cells (name, thumbnail image).
func fetchCard() {
cards = []
let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Card")
let sorter: NSSortDescriptor = NSSortDescriptor(key: sorterParameter , ascending: true)
fetchRequest.predicate = definitivePredicate
fetchRequest.sortDescriptors = [sorter]
fetchRequest.returnsObjectsAsFaults = true
do {
let results = try moc.executeFetchRequest(fetchRequest)
cards = results as! [Card]
} catch {
print("Fetch failed: \(error)")
}
}
Is there a way to load, for example, only the visible rows, and would this work/is it a good practice to do that? Are there better ways?
P.S.: To avoid a discussion based on personal preferences, for "better" I mean the way with the highest performances in terms of loading speed.
EDIT: Added code example.
The problem is that you're loading many data in viewDidLoad() method and you are not loading them asynchronously on background thread. When you call fetchCard() in viewDidLoad() you block loading of the whole screen until loading from DB is done.
To load data on background thread you will have to do this:
let queue: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)
dispatch_async(queue, { () -> Void in
let context: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType)
context.parentContext = moc
let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Card")
let sorter: NSSortDescriptor = NSSortDescriptor(key: sorterParameter , ascending: true)
fetchRequest.predicate = definitivePredicate
fetchRequest.sortDescriptors = [sorter]
fetchRequest.returnsObjectsAsFaults = true
do {
let results = try context.executeFetchRequest(fetchRequest)
cards = results as! [Card]
} catch {
print("Fetch failed: \(error)")
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
tableView.reloadData()
})
})
Also if loading from CoreData gets slow you will have to find out why. It might help to add indexes to variables in objects which you sort/filter by. In sortDescriptors you add 1 descriptor, try to index the sorterParameter which is used in the descriptor.
If you have images stored in CoreData database, it might be wise to load them asynchronously as well to keep the scrolling of tableview smooth. You can use similar code as before, just move the image loading inside the GCD block and set the image to the imageview in main thread ('dispatch_async(dispatch_get_main_queue()...').
From what the OP has stated the problem solution is to create an index on the attribute (column) that is being used by the sore.
See this SO Question for some more information. Then follow up with the Core Data docs or a good Core Data book.
Notes:
1. The If the table row count is around 1000 as the OP has hinted the access time with an indexed Core Data should be almost instantaneous.
The time to regenerate an image of 50 x 50 pixels is should also be trivial.
On performance issues it in important to measure where the performance issue is otherwise time and effort is spend on the wrong areas.
UITableView already only loads visible cells in cellForRowAtIndexPath. Assuming you're reusing cells, your problem is most likely in loading the data, not displaying it.
Take a look at Apple's sample project for asynchronous loading: LazyTableImages
Resizing a large image to fit a smaller table view cell size image does take some processing power and a decent amount of IO. If possible, you should use smaller thumbnail images for the table view, rather than large images. Though, it may take a significant size image to notice this on newer devices.

Error creating a separate NSManagedObjectContext

Before getting into my issue, please have a look at this image.
Here is the actual data model:
I retrieve a set of Records from a web API, create objects out of them, save them in core data and display them in the Today view. By default these records are returned for the current date.
The user can tap on Past button to go to a separate view where he can choose a past or future date from a date picker view and view Records for that selected date. This means I have to call the API again passing the selected date, retrieve the data and save that data in core data and display them. When the user leaves this view, this data should be discarded.
This is the important part. Even though I get a new set of data, the old original data for the current date in the Today view must not go away. So if/when the user returns to the Today view, that data should be readily available as he left it without the app having to call the API and get the data for the current date again.
I thought of creating a separate NSManagedObjectContext to hold these temporary data.
I have a separate class called DatabaseManager to handle core data related tasks. This class initializes with an instance of `NSManagedObjectContext. It creates the managed object classes in the given context.
import CoreData
import Foundation
import MagicalRecord
import SwiftyJSON
public class DatabaseManager {
private let context: NSManagedObjectContext!
init(context: NSManagedObjectContext) {
self.context = context
}
public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
let json = JSON(data)
if let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context) as Record
record.id = recordObj["Id"].int
record.name = recordObj["Name"].string!
record.date = NSDate(string: recordObj["Date"].string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}
So in the Today view I pass NSManagedObjectContext.MR_defaultContext() to insertRecords() method. I also have a method to fetch Records from the given context.
func fetchRecords(context: NSManagedObjectContext) -> [Record]? {
return Record.MR_findAllSortedBy("name", ascending: true, inContext: context) as? [Record]
}
The data is retrieved from the API, saved in core data and gets displayed successfully. All good so far.
In the Past View, I have to do basically the same thing. But since I don't want the original data to change. I tried to do this a few ways which MagicalRecord provides.
Attempt #1 - NSManagedObjectContext.MR_context()
I create a new context with NSManagedObjectContext.MR_context(). I change the date in Past view, the data for that selected date gets retrieved and saved in the database successfully. But here's the issue. When I fetch the objects from core data, I get that old data as well. For example, each day has only 10 records. In Today view I display 10 records. When the fetch objects in the Past view, I get 20 objects! I assume it's the old 10 objects plus the new ones. Also when I try to display them in the tableview, it crashes with a EXC_BAD_ACCESS error in the cellForRowAtIndexPath method.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let record = records[indexPath.row]
cell.textLabel?.text = record.name // EXC_BAD_ACCESS
cell.detailTextLabel?.text = record.date.toString()
return cell
}
Attempt #2 - NSManagedObjectContext.MR_newMainQueueContext()
The app crashes when I change the date with the following error.
'+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Record''
Attempt #3 - NSManagedObjectContext.MR_contextWithParent(NSManagedObjectContext.MR_defaultContext())
Same result as Attempt #1.
Attempt #4 - From Hal's Answer I learned that even though I create two MOCs, they both refer to the same NSPersistentStore. So I created another new store to hold the temporary data in my AppDelegate.
MagicalRecord.setupCoreDataStackWithStoreNamed("Records")
MagicalRecord.setupCoreDataStackWithStoreNamed("Records-Temp")
Then when I change the date to get the new data, I set that temporary store as the default store like this.
func getDate(date: NSDate) {
let url = NSPersistentStore.MR_urlForStoreName("Records-Temp")
let store = NSPersistentStore(persistentStoreCoordinator: NSPersistentStoreCoordinator.MR_defaultStoreCoordinator(), configurationName: nil, URL: url, options: nil)
NSPersistentStore.MR_setDefaultPersistentStore(store)
let context = NSManagedObjectContext.MR_defaultContext()
viewModel.populateDatabase(date, context: context)
}
Note that I'm using the default context. I get the data but it's the same result as Attempt 1 and 3. I get 20 records. They include data from both the old date and the new date. If I use NSManagedObjectContext.MR_context(), it would simply crash like in Attempt 1.
I also discovered something else. After creating the stores in App Delegate, I printed out the default store name println(MagicalRecord.defaultStoreName()) in the Today's view. Strangely it didn't print the name I gave the store which is Records. Instead it showed Reports.sqlite. Reports being the project's name. Weird.
Why do I get the old data as well? Am I doing something with when initializing a new context?
Sorry if my question is a little confusing so I uploaded a demo project to my Dropbox. Hopefully that will help.
Any help is appreciated.
Thank you.
Thread Safety
First of all I want to mention the Golden Rule of Core Data. NSManagedObject's are not thread safe, hence, "Thou shalt not cross the streams" (WWDC). What this means is that you should always access a Managed Object in its context and never pass it outside of its context. This is why your importer class worries me, you are inserting a bunch of objects into a context without guaranteeing that you are running the insert inside the Context.
One simple code change would fix this:
public func insertRecords(data: AnyObject, success: () -> Void, failure: (error: NSError?) -> Void) {
let json = JSON(data)
context.performBlock { () -> Void in
//now we are thread safe :)
if let records = json.array {
for recordObj in records {
let record = Record.MR_createInContext(context) as Record
record.id = recordObj["Id"].int
record.name = recordObj["Name"].string!
record.date = NSDate(string: recordObj["Date"].string!)
}
context.MR_saveToPersistentStoreAndWait()
success()
}
}
}
The only time you don't need to worry about this is when you are using the Main Queue Context and accessing objects on the main thread, like in tableview's etc.
Don't forget that MagicalRecord also has convenient save utilities that create context's ripe for saving :
MagicalRecord.saveWithBlock { (context) -> Void in
//save me baby
}
Displaying Old Records
Now to your problem, the following paragraph in your post concerns me:
The user can tap on Past button to go to a separate view where he can
choose a past or future date from a date picker view and view Records
for that selected date. This means I have to call the API again
passing the selected date, retrieve the data and save that data in
core data and display them. When the user leaves this view, this data
should be discarded.
I don't like the idea that you are discarding the information the user has requested once they leave that view. As a user I would expect to be able to navigate back to the old list and see the results I just queried without another unecessary network request. It might make more sense to maybe have a deletion utility that prunes your old objects on startup rather than while the user is accessing them.
Anyways, I cannot illustrate how important it is that you familiarize yourself with NSFetchedResultsController
This class is intended to efficiently manage the results returned from
a Core Data fetch request.
You configure an instance of this class using a fetch request that
specifies the entity, optionally a filter predicate, and an array
containing at least one sort ordering. When you execute the fetch, the
instance efficiently collects information about the results without
the need to bring all the result objects into memory at the same time.
As you access the results, objects are automatically faulted into
memory in batches to match likely access patterns, and objects from
previous accessed disposed of. This behavior further serves to keep
memory requirements low, so even if you traverse a collection
containing tens of thousands of objects, you should never have more
than tens of them in memory at the same time.
Taken from Apple
It literally does everything for you and should be your go-to for any list that shows objects from Core Data.
When I fetch the objects from core data, I get that old data as well
Thats to be expected, you haven't specified anywhere that your fetch should include the reports in a certain date range. Here's a sample fetch:
let fetch = Record.MR_createFetchRequest()
let maxDateForThisController = NSDate()//get your date
fetch.predicate = NSPredicate(format: "date < %#", argumentArray: [maxDateForThisController])
fetch.fetchBatchSize = 10// or an arbitrary number
let dateSortDescriptor = NSSortDescriptor(key: "date", ascending: false)
let nameSortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetch.sortDescriptors = [dateSortDescriptor,nameSortDescriptor]//the order in which they are placed in the array matters
let controller = NSFetchedResultsController(fetchRequest: fetch,
managedObjectContext: NSManagedObjectContext.MR_defaultContext(),
sectionNameKeyPath: nil, cacheName: nil)
Importing Discardable Records
Finally, you say that you want to see old reports and use a separate context that won't save to the persistent store. Thats also simple, your importer takes a context so all you would need to do is make sure that your importer can support imports without saving to the persistent store. That way you can discard the context and the objects will go with it. So your method signature could look like this:
public func insertRecords(data: AnyObject, canSaveToPersistentStore: Bool = true,success: () -> Void, failure: (error: NSError?) -> Void) {
/**
Import some stuff
*/
if canSaveToPersistentStore {
context.MR_saveToPersistentStoreWithCompletion({ (complete, error) -> Void in
if complete {
success()
} else {
error
}
})
} else {
success()
}
}
The old data that was in your persistent store, and addressed with the original MOC, is still there, and will be retrieved when the second MOC does a fetch. They're both looking at the same persistent store. It's just that the second MOC also has new data fetched from your API.
A synchronous network operation saving to Core Data will hang your app, and (for a large enough set of records) cause the system to kill your app, appearing to the user as a crash. Your client is wrong on that point, and needs to be educated.
Break apart your logic for fetching, saving, and viewing. Your view that shows a particular date's records should just do that--which it can do, if it accepts a date and uses a predicate.
Your 'cellForRowAtIndexPath' crash smells like a problem with a missing or misspelled identifier. What happens if you hard code a string instead of using 'record.name'?

Resources