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.
Related
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?
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.
I'm struggling a little bit trying to create an application for my own education purposes using Swift.
Right now I have the following (desired) order of execution:
TabView
FirstViewController - TableView
Check into CoreData
If data exists update an array using a closure
If data doesn't exists then download it using Alamofire from API and store it into Core Data
SecondViewController - CollectionView
Checks if data of images exists in Core Data, if it does, loads it from there, otherwise download it.
The problem that I'm struggling the most is to know if the code after a closure is executed after (synchronously) the closure ends or it might be executed before or while the closure is executed.
For example:
FirstViewController
var response: [DDGCharacter]
//coreData is an instance of such class
coreData.load(onFinish: { response in //Custom method in another class
print("Finished loading")
self.response = response
})
print("Executed after loading data from Core Data")
//If no data is saved, download from API
if response.count == 0 {
//Download from API
}
I have done the above test with the same result in 10 runs getting:
Finished loading
Executed after loading data from Core Data
In all 10, but it might be because of load is not taking too much time to complete and thus, appear to be synchronous while it's not.
So my question is, is it going to be executed in that order always independent of amount of data? Or it might change? I've done some debugging as well and both of them are executed on the main thread as well. I just want to be sure that my suppositions are correct.
As requested in the comments, here's the implementation done in the load() method:
func load(onFinish: ([DDGCharacter]) -> ()) {
var characters: [DDGCharacter] = []
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject> (entityName: "DDGCharacter")
do {
characters = try managedContext.fetch(fetchRequest) as! [DDGCharacter]
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
onFinish(characters)
}
Your implementation of load(onFinish:) is very surprising and over-complicated. Luckily, though, that helps demonstrate the point you were asking about.
A closure is executed when something calls it. So in your case, onFinish is called at the end of the method, which makes it synchronous. Nothing about being "a closure" makes anything asynchronous. It's just the same as calling a function. It is completely normal to call a closure multiple times (map does this for instance). Or it might never be called. Or it might be called asynchronously. Fundamentally, it's just like passing a function.
When I say "it's slightly different than an anonymous function," I'm just referring to the "close" part of "closure." A closure "closes over" the current environment. That means it captures variables in the local scope that are referenced inside the closure. This is slightly different than a function (though it's more about syntax than anything really deep; functions can actually become closures in some cases).
The better implementation would just return the array in this case.
I am creating a simple quiz app. I am planning to show some kind of "history" where the user can see the following:
Date and time of playing
Score for that particular session
How do I do that?
As of the Date and Time of playing, I saw this thread on SO: How to get the current time as datetime
However, how do I "RECORD" the date(s) and time(s) the user played the game?
Regarding the Score data, I am using:
NSUserDefaults.standardUserDefaults().setInteger(currentScore, forKey: "score")
However, I am only able to get the CURRENT SCORE. How do I record the score(s) the user got for EACH session on different date(s) and time(s)?
Please note that I have no problem in getting the user's CURRENT SCORE. I need help in storing or recording the user's score(s) in multiple sessions.
For instance, I wanted to display something like this:
Date: 2/7/16
Time: 7:00 AM
Score: 70/100
NSUserDefaults probably isn't right for what you are trying to do. I recommend using NSCoding for simple data storing. Core Data may be too complicated for something this simple. However, if you plan on saving a large data model with relationships, Core Data is the way to go.
NSCoding
NSCoding has two parts:
Encoding and decoding
Archiving and unarchiving
NSHipster explains this perfectly:
NSCoding is a simple protocol, with two methods: -initWithCoder: and encodeWithCoder:. Classes that conform to NSCoding can be serialized and deserialized into data that can be either be archived to disk or distributed across a network.
That archiving is performed by NSKeyedArchiver and NSKeyedUnarchiver.
Session
Even without NSCoding, it is suggested to represent data with objects. In this case, we can use the very creative name Session to represent a session in the history.
class Session: NSObject, NSCoding {
let date: NSDate // stores both date and time
let score: Int
init(date: NSDate, score: Int) { // initialize a NEW session
self.date = date
self.score = score
super.init()
}
required init?(coder aDecoder: NSCoder) { // decodes an EXISTING session
if let decodedDate = aDecoder.decodeObjectForKey("date") as? NSDate {
self.date = decodedDate
} else {
self.date = NSDate() // placeholder // this case shouldn't happen, but clearing compiler errors
}
self.score = aDecoder.decodeIntegerForKey("score")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(date, forKey: "date")
aCoder.encodeInteger(score, forKey: "score")
}
}
The above code in English, in order from top to bottom:
Defining the class, conforming to NSCoding
The properties of a session: the date (+ time) and the score
The initializer for a new session - simply takes a date and score and creates an session for it
The required initializer for an existing session - decodes the date and score that is saved
decodeObjectForKey: simply does what it says (decodes an object using a key), and it returns AnyObject?
decodeIntegerForKey:, however, returns Int. If none exists on file, it returns 0, which is why it isn't optional. This is the case for most of the decoding methods except for decodeObjectForKey:
The required method for encoding an existing session - encodes the date and score
The encoding methods are just as straightforward as the decoding methods.
That takes care of the Session class, with the properties ready for NSCoding. Of course, you could always add more properties and methods.
SessionHistory
While the sessions itself are nice, an object to manage the array of sessions is needed, and it also needs to conform to NSCoding. You could also add this code to an existing class.
class SessionHistory: NSObject, NSCoding {
var sessions = [Session]()
required init?(coder aDecoder: NSCoder) {
if let decodedSessions = aDecoder.decodeObjectForKey("sessions") as? [Session] {
self.sessions = decodedSessions
} else {
self.sessions = [] // another compiler error clearer
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(sessions, forKey: "sessions")
}
override init() { // Used for convenience
super.init()
}
}
English translation:
Defining the manager, conforming to NSCoding
Add property for the array of sessions
Next two NSCoding methods do nearly the same thing as Session. Except this time, it is with an array.
Initializer for a new manager, which will be used below.
NSCoding looks at this manager class and sees that it needs to encode an array of sessions, so then NSCoding looks at the Session class to see what to encode for those sessions.
NSKeyedArchiver/NSKeyedUnarchiver and Singletons
While all the NSCoding is set up now, the final step is to incorporate NSKeyedArchiver and NSKeyedUnarchiver to actually save and load the data.
The two important methods are NSKeyedArchiver.archiveRootObject(_, toFile:) and NSKeyedUnarchiver.unarchiveRootObjectWithFile:
Note that both methods need a file. It automagically creates the file for you, but you need to set a location. Add this to SessionHistory:
static var dataPath: String {
let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let URL = URLs[0]
return URL.URLByAppendingPathComponent("savehistory").path! // Put anything you want for that string
}
That simply finds a location for the file. You could, of course, find somewhere else to put the file.
With the data path ready, you can use the two methods I mentioned earlier. I like to use a modified version of a singleton for the manager class to make sure I'm using the same array of objects. In the SessionHistory class:
private static var history: SessionHistory!
static func appHistory() -> SessionHistory {
if history == nil {
if let data = NSKeyedUnarchiver.unarchiveObjectWithFile(dataPath) as? SessionHistory {
history = data
} else {
history = SessionHistory()
}
}
return history
}
This creates a private static property to store the one session history of the app. The static method checks if the session history is nil. If so, it returns the current history on file and loads the file into the history property. Otherwise, it creates a new empty session history. After that, or if the history property already stores something, it returns the history property.
Usage
All the setup for NSCoding and NSKeyedArchiver is done. But how do you use this code?
Each time you want to access the session history, call
SessionHistory.appHistory()
Wherever you want to save the session history, call
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
Sample usage would work like this:
let session = Session(date: someRandomDate, score: someRandomScore)
SessionHistory.appHistory().sessions.append(session)
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
The session history will automatically be loaded from the file when accessed via SessionHistory.appHistory().
You don't really need to "link" the classes per se, you just need to append the sessions to the sessions array of the session history.
Further Reading
NSHipster is a good, simple introduction to NSCoding.
Apple's NSCoding guide, although older and in Objective-C, goes deeper into NSCoding.
To store scores for each session, you'll need some sort of data structure associated with each user. You could use a dictionary of key value pairs that associates a date with a score. That way you would have a score for each date stored for the user.
You need to use a database to store such data with an undefined count of records.
Check out the Core Data Programming Guide by Apple here
You could then create an entity named history where you store records of the user's game play history, by inserting a new object into the entity every time the user finishes a game.
When you need to show the results, you'd create an NSFetchRequest over an NSManagedObjectContext to get all the results and display/filter them as you'd like.
Hope that helps!
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'?