Coredata duplicates all objects in a list on update - ios

I've come across an issue which rarely happens, and (of course) it works perfectly when I test it myself. It has only happened for a few users, and I know I have at least a couple hundred who use the same App daily.
The issue
When updating a list of coredata objects in a tableview, it not only updates the objects (correctly), it also creates duplicates of all these objects.
Coredata setup
It's a NSPersistentCloudKitContainer with these settings:
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Tableview setup
I have a tableview which displays a list of the 'ActivityType' objects. It's very simple, they have a name (some other basic string/int properties), and an integer called 'index'. This 'index' exists so that users can change the order in which they should be displayed.
Here is some code for how each row is setup:
for activityType in activityTypes {
row = BlazeRow()
row.title = activityType.name
row.cellTapped = {
self.selectedActivityType(activityType)
}
row.object = activityType
row.cellReordered = {
(index) in
self.saveNewOrder()
}
section.addRow(row)
}
As you can see, it has 2 methods. One for selecting the activity which shows its details in a new viewcontroller, and one which is called whenever the order is changed.
Here's the method that is called whenever the order is changed:
func saveNewOrder() {
Thread.printCurrent()
let section = self.tableArray[0] as! BlazeSection
for (index, row) in section.rows.enumerated() {
let blazeRow = row as! BlazeRow
let object = blazeRow.object as! ActivityType
object.index = Int32(index)
}
BDGCoreData.saveContext()
}
And here's the code that saves the context (I use a singleton to easily access the viewcontext):
class func saveContext(context: NSManagedObjectContext = BDGCoreData.viewContext) {
if(context.hasChanges) {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
Now, I swear to god it never calls the method in this viewcontroller to create a new object:
let activity = ActivityType(context: BDGCoreData.viewContext). I know how that works, and it truly is only called in a completely different view controller. I searched for it again in my entire project just in case, and it's really never called/created in any other places.
But somehow, in very rase cases, it saves the correct new order but also duplicates all objects in this list. Since it only happens rarely, I thought it might have something to do with threads? Which is why, as you can see in the code, I printed out the current thread, but at least when testing on my device, it seems to be on the main thread.
I'm truly stumped. I have a pretty good understanding of coredata and the app itself is quite complex with full of objects with different kind of relationships.
But why this happens? I have no clue...
Does anyone have an idea?

Related

Why would there be inconsistency when saving NSManagedObjectContext after adding NSSecureUnarchiveFromDataTransformer?

I have an app that uses Core Data to persist a store of Events.
An Event has an optional location (stored as a CLLocation) as one of its attributes. In my model, this location attribute has the type Transformable:
My app has been in production for several years and everything's been working reliably, but some time in the past year I started getting an error in the Xcode console telling me I should switch to using "NSSecureUnarchiveFromData" or a subclass of NSSecureUnarchiveFromDataTransformer instead.
After doing some research (I'd consider myself a novice at Core Data) I determined I should write a NSSecureUnarchiveFromDataTransformer subclass and put the name of that class in the Transformer field for the location attribute, which was blank, with the Value Transformer Name placeholder text:
From what I found online, the subclass could be pretty straightforward for a Transformable attribute that contains a CLLocation:
#objc(CLLocationValueTransformer)
final class CLLocationValueTransformer: NSSecureUnarchiveFromDataTransformer {
static let name = NSValueTransformerName(rawValue: String(describing: CLLocationValueTransformer.self))
override static var allowedTopLevelClasses: [AnyClass] {
return [CLLocation.self]
}
public static func register() {
let transformer = CLLocationValueTransformer()
ValueTransformer.setValueTransformer(transformer, forName: name)
}
}
So, I made this subclass in my project and put the class name in the Transformer field for the location attribute:
But now, here's the problem:
Once I started using my app after implementing the Transformer, I started getting unpredictable results.
Sometimes, when I created a new Event, it disappeared at the next app launch. It was not persisted by Core Data across app launches, like it was before the change.
Sometimes Events were saved, but sometimes they were not.
I couldn't figure out a clear pattern. They were not always saved if the location was included when it was first created, but sometimes they were. It seemed like other times the original Event was saved, but without location if the location was added later.
I've left out the boilerplate Core Data code, but basically I have baseManagedObjectContext, which is the layer that's connected to the NSPersistentStoreCoordinator to save data to disk.
baseManagedObjectContext is a parent to mainObjectContext, which is used by most of my UI. Then I create private contexts to write changes to first before saving them.
Here is example code to create a new Event with a possible location and save it, which was working consistently for years before adding the NSSecureUnarchiveFromDataTransformer subclass as the Transformer on location. I added fatalError for debugging, but it was never called, even when my data didn't fully save to disk:
private func addEvent(location: CLLocation?) {
let privateLocalContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
privateLocalContext.parent = coreDataStack.mainObjectContext
privateLocalContext.undoManager = nil
let entity = NSEntityDescription.entity(forEntityName: "Event",
in: privateLocalContext)
let newEvent = Event(entity: entity!,
insertInto: privateLocalContext)
if let location = location {
newEvent.location = location
}
privateLocalContext.performAndWait {
do {
try privateLocalContext.save()
}
catch { fatalError("error saving privateLocalContext") }
}
coreDataStack.mainObjectContext.performAndWait {
do {
try coreDataStack.mainObjectContext.save()
}
catch { fatalError("error saving mainObjectContext") }
}
coreDataStack.baseManagedObjectContext.perform {
do {
try coreDataStack.baseManagedObjectContext.save()
}
catch { fatalError("error saving baseManagedObjectContext") }
}
}
With further debugging, I found that sometimes, even if the change was making it to mainObjectContext, it was not making it all the way to baseManagedObjectContext, or to disk. I created a whole separate Core Data stack for testing to read directly from disk.
This issue with the saves not propogating (like they did before, for years) is what was causing the data to not persist across app launches, but I do not understand why this would suddenly start happening after my addition of the Transformer on location.
What am I missing here with how Core Data works?
I did not think I was fundamentally changing anything when I switched from a blank Transformer field to a subclass of NSSecureUnarchiveFromDataTransformer, but clearly something is going on that I don't understand.
I'd like to adopt NSSecureUnarchiveFromDataTransformer, since Apple is recommending it. How can I change what I'm doing to be able to adopt it and have data save consistently?
For now, I've switched back to the blank Transformer field to keep things working like they did before.

Core Data seems to delete my data right after insertion

I have a lot of data stored in Core Data without any trouble. Now I need to add some additional data. Instead of creating an entity for all this new data, I have decided to use the fact that the object to be stored(and all its children) implement NSCoding, and rather store the object as a Transformable (NSObject). I have done this before (in Obj-c), but for some reason I can't get it to work this time.
Let's say I have a huge class named Event:NSObject,NSCoding which contains name, date, and a metric ton of additional variables. Then imagine you'd want the ability to let the user receive a notification a given number of days before the event starts. Like, let the user "watch" the event. I want to keep track of which events are being watched, and how long before I should send the notification. With this, I can get a list of all the "watched" events, and know how many days before the event they want a notification. This is just a poor example of the real situation, just bear with me. Don't think about the "notification" part, just storing the data.
I have my Event-object, now I have created a WatchEvent-entity in my CoreData-database which has two attributes: event:Transformable and days:Integer. Neither are optional.
My entire Event-class and all its children now implement NSCoding, so to store this in the database, I simply set it to the transformable attribute. Here you can see how I create the WatchEvent-object and put it in the database, and a function to get all WatchEvents from the DB, and a function to print out the contents of each WatchEvent.
func storeWatchEvent(someEvent:Event, numberOfDays:Int){
let watchEvent = WatchEvent(entity: NSEntityDescription.entity(forEntityName: "WatchEvent", in: managedObjectContext)!, insertInto: managedObjectContext)
watchEvent.days = numberOfDays //e.g 3
watchEvent.event = someEvent
saveContext()
}
func saveContext(){
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func getWatchedEvents()->[WatchEvent]?{
return managedObjectContext.fetch(WatchEvent.fetchRequest())
}
func printOutAllWatchedEvents(){
if let watchedEvents = getWatchedEvents(){
watchedEvents.foreach{ (w) in
print("numberOfDays: ", w.days)
print("event: ", w.event)
}
}
}
func saveButtonClicked(){
storeWatchEvent(someEvent, numberOfDays:3)
// For testing, attempt to get all my events immediately, several times
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
}
func verifyButtonClicked(){
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and `nil` for event.
}
Let's say I have two buttons. One "Save" and another "Verify". If I click "Save", I save the valid objects, and as you can see in the code, I immediately query the database for all stored WatchEvents, and print them out. At that time, everything looks good. If I click the "verify"-button, it should've printed out the same thing, but it has deleted my event. It manages to keep the days-attribute stored, but the event is deleted. Why?:(
It doesn't matter how long I wait to click the verify-button. If I click them nearly at the same time, this still happens.
I call printOutAllWatchedEvents in saveButtonClick, three times, and since it manages to return a valid event-object every time, I assume that the storing/NSCoding-part of this was successful?
But if I click the verify-button, which I assume will happen at least a few "run loops" later, the transformable event-object has been deleted..
I have no idea what's happening.
Why does it manage to return my valid events if I request them immediately after inserting them, but not if I request them later? Why does this only affect the transformable object? The integer days is correctly retained for all WatchEvents. And since I have marked event as not optional, how can it return nil and never give me any errors? There are no errors when I call save on the context, I have checked.
I figured it out.. It had nothing to do with CoreData, really. It was a faulty implementation of NSCoding. I never got any errors of any kind, so it was hard to figure out, especially with so many variables, but the problem was essentially this:
class Event:NSObject,NSCoding{
let hasBeer:Bool
func encode(with aCoder: NSCoder) {
aCoder.encode(hasBeer, forKey: "hasBeer")
}
required init?(coder aDecoder: NSCoder) {
//This line:
self.hasBeer = aDecoder.decodeObject(forKey: "hasBeer") as? Bool ?? false
//Should be like this:
self.hasBeer = aDecoder.decodeBool(forKey: "hasBeer")
//Because it is a primitive type Bool, and not an Object of any kind.
super.init()
}
}
I'm still not entirely sure why I experienced it like this though..
The encoding always succeeded, so the Event was stored in the database (even though it looked empty when inspecting the blob-field in the sqlite-file using Liya). It succeeded encoding because the functions are named the same for all types (Bool, Int, Double, and NSObject). They all work with aCoder.encode(whatever, forKey: "whatever"). However, when DEcoding them, you have to pick the right decoding-function, e.g decodeBool, decodeInt32, decodeObject etc.
As I stated, I have done this in the past, in Objective-C, where I had no trouble at all, because both the encoding and decoding-functions are named for the type ([aDecoder decodeBoolForKey:#"key"]; and [aCoder encodeBool:hasBeer, forKey:#"hasBeer"];), and threw compile-time-errors when using the wrong one.
I guess I would've picked up on this if I had some fatalError("something") in my required init? instead of just using return nil when something didn't go as planned.

Firebase query observing reshowing data

I have a firebase query that observes data from a posts child.
func fetchPosts () {
let query = ref.queryOrdered(byChild: "timestamp").queryLimited(toFirst: 10)
query.observe(.value) { (snapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
if let value = child.value as? NSDictionary {
let post = Post()
let poster = value["poster"] as? String ?? "Name not found"
let post_content = value["post"] as? String ?? "Content not found"
let post_reveals = value["Reveals"] as? String ?? "Reveals not found"
post.post_words = post_content
post.poster = poster
post.Reveals = post_reveals
self.postList.append(post)
DispatchQueue.main.async { self.tableView.reloadData() }
//make this for when child is added but so that it also shows psots already there something like query.observre event type of
}
}
However, when a user posts something, it creates a more than one cell with the data. For instance, if I post "hello", a two new cards show up with the hello on it. However, when I exit the view and recall the fetch posts function, it shows the correct amount of cells. Also, when I delete a post from the database, it adds a new cell as well and creates two copies of it until I reload the view, then it shows the correct data from the database.
I suspect this has something to do with the observe(.value), as it might be getting the posts from the database and each time the database changes it creates a new array. Thus, when I add a new post, it is adding an array for the fact that the post was added and that it now exists in the database, and when I refresh the view it just collects the data directly from the database.
Also, sometimes the correct amount of cells show and other times there's multiple instances of random posts, regardless of whether I have just added them or not.
How can I change my query so that it initially loads all the posts from the database, and when some post is added it only creates one new cell instead of two?
Edit: The logic seeming to occur is that when the function loads, it gets all the posts as it calls the fetchPosts(). Then, when something is added to the database, it calls the fetchPosts() again and adds the new data to the array while getting all the old data. yet again.
One thing I always do when appending snapshots into an array with Firebase is check if it exists first. In your case I would add
if !self.postList.contains(post) {
self.postList.append...
however, to make this work, you have to make an equatable protocol for what I'm guessing is a Post class like so:
extension Post: Equatable { }
func ==(lhs: Post, rhs: Post) -> Bool {
return lhs.uid == rhs.uid
}
You are right in thinking that the .value event type will return the entire array each time there is a change. What you really need is the query.observe(.childAdded) listener. That will fetch individual posts objects rather than the entire array. Call this in your viewDidAppear method.
You may also want to implement the query.observe(.childRemoved) listener as well to detect when posts are removed.
Another way would be to call observeSingleEvent(.value) on the initial load then add a listener query.queryLimited(toLast: 1).observe(.childAdded) to listen for the latest post.

How to Update uitableview from CloudKit using discoverAllIdentities

So, I am new to cloudKit and to working with multiple threads in general, which I think is the source of the problem here, so if I simply need to research more, please just comment so and I will take that to heart.
Here is my question:
I am working in Swift 3 Xcode 8.1
I have in my view controller this variable:
var contactsNearby: [String:CLLocation]?
Then at the end of ViewDidLoad I call one of my view controllers methods let's call it:
populateContactsNearby()
inside that method I call:
container.discoverAllIdentities(completionHandler: { (identities, error) in
for userIdentity in identities! {
self.container.publicCloudDatabase.fetch(withRecordID: userIdentity.userRecordID!, completionHandler: { (userRecord, error) in
let contactsLocation = userRecord?.object(forKey: "currentLocation")
if self.closeEnough(self.myLocation!, contactLocation: contactsLocation as! CLLocation) {
var contactsName = ""
contactsFirstName = userIdentity.nameComponents?.givenName
if contactsName != "" && contactsLocation != nil {
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
}
}
})
}
})
}
I apologize if I am missing or have an extra bracket somewhere. I have omitted some error checking code and so forth in order to get this down to bare-bones. So the goal of all that is to populate my contactsNearby Dictionary with data from CloudKit. A name as the key a location as the value. I want to use that data to populate a tableview. In the above code, the call to closeEnough is a call to another one of my view controllers methods to check if the contact from CloudKit has a location close enough to my user to be relevant to the apps purposes. Also myLocation is a variable that is populated before the segue. It holds the CLLocation of the app users current location.
The Problem:
The if statement:
if contactsName != "" && contactsLocation != nil { }
Appears to succeed. But my view controllers variable:
var contactsNearby: [String:CLLocation]?
Is never populated and I know there is data available in cloudKit.
If it's relevant here is some test code that I have in cellForRowAtIndexPath right now:
let contact = self.contactsNearby?.popFirst()
let name = contact?.key
if name != nil {
cell.textLabel?.text = name
}else {
cell.textLabel?.text = "nothing was there"
}
My rows alway populate with "nothing was there". I have seen answers where people have done CKQueries to update the UI, but in those answers, the user built the query themselves. That seems different from using a CloudKit function like discoverAllIdentities.
I have tried to be as specific as possible in asking this question. If this question could be improved please let me know. I think it's a question that could benefit the community.
Okay, I need to do some more testing, but I think I got it working. Thank you Paulw11 for your comment. It got me on the right track.
As it turns out there were 2 problems.
First, as pointed out I have an asynchronous call inside a for loop. As recommended, I used a dispatchGroup.
Inside the cloudKit call to discoverAllIdentities I declared a dispatchGroup, kind of like so:
var icloudDispatchGroup = DispatchGroup()
Then just inside the for loop that is going to make an async call, I enter the dispatchGroup:
icloudDispatchGroup.enter()
Then just before the end of the publicCloudDatabase.fetch completion handler I call:
icloudDispatchGroup.leave()
and
icloudDispatchGroup.wait()
Which, I believe, I'm still new to this remember, ends the dispatchGroup and causes the current thread to wait until that dispatchGroup finishes before allowing the current thread to continue.
The Above took care of the multithreading issue, but my contactsNearby[String:CLLocation]? Dictionary was still not being populated.
Which leads me to the 2nd problem
At the top of my view controller I declared my Dictionary:
var contactsNearby: [String: CLLocation]?
This declared a dictionary, but does not initialize it, which I don't think I fully realized, so when I attempted to populate it:
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
It quietly failed because it is optional and returned nil
So, in viewDidLoad before I even call populateContactsNearby I initialize the dictionary:
contactsNearby = [String:CLLocation]()
This does not make it cease to be an optional, which Swift being strongly typed would not allow, but merely initializes contactsNearby as an optional empty Dictionary.
At least, that is my understanding of what is going on. If anyone has a more elegant solution, I am always trying to improve.
In case you are wondering how I then update the UI, I do so with a property observer on the contactsNearby Dictionary. So the declaration of the dictionary at the top of the view controller looks like this:
var contactsNearby: [String: CLLocation]? {
didSet {
if (contactsNearby?.isEmpty)! || contactsNearby == nil {
return
}else{
DispatchQueue.main.sync {
self.nearbyTableView.reloadData()
}
}
}
}
I suppose I didn't really need to check for empty and nil. So then in cellForRowAtIndexPath I have something kind of like so:
let cell = tableview.dequeueReusableCell(withIdentifier: "nearbyCell", for: indexPath)
if contactsNearby?.isEmpty == false {
let contact = contactsNearby?.popFirst()
cell.textLabel?.text = contact?.key
}else {
cell.textLabel?.text = "Some Placeholder Text Here"
}
return cell
If anyone sees an error in my thinking or sees any of this heading for disaster, feel free to let me know. I still have a lot of testing to do, but I wanted to get back here and let you know what I have found.

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