I'm trying to make an iOS 8 App with Swift and i need to download data from JSON and save it , but i don't understand Core Data mechanism. (I'm coming from Android with ORM Lite and Windows Phone with sqlite-net).
I'm trying to make two tasks, "GetAllNewsTask" returning all News from database , and "UpdateAllNewsTask" downloading JSON and parsing it, save to database and return all News.
The function getEntitiesFromJson transform parsed JSON string to entity object
class func getEntitiesFromJson(json: JSONValue) -> [NewsEntity]?{
var rList : [NewsEntity] = []
var array = json.array
var countItr = array?.count ?? 0
if(array == nil){
return nil
}
if(countItr > 0){
for index in 0...countItr-1{
var news = NewsEntity()
var jsonVal = array?[index]
news.id = jsonVal?["id"].integer ?? 0
........
rList.append(news)
}
}
return rList
}
GetAllNewsTask (newsDao.findAll() currently return an harcoded empty array, i didn't found how to select all NewsEntity synchronously)
class GetAllNewsTask:NSOperation {
var result : Array<News>?
override func main() -> (){
result = executeSync()
}
func executeSync() -> Array<News>? {
let newsDao = NewsDAO()
let entities = newsDao.findAll()
return NewsModel.getVOsFromEntities(entities)
}
UpdateAllNewsTask
class UpdateAllNewsTask:NSOperation {
var result : Array<News>?
override func main() -> (){
result = executeSync()
}
func executeSync() -> Array<News>? {
let response = JsonServices.getAllNews()
var managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var entityDescription = NSEntityDescription.entityForName("NewsEntity", inManagedObjectContext: managedObjectContext)
var entities = NewsModel.getEntitiesFromJson(response)
//TODO insert new, update existing and remove old
return GetAllNewsTask().executeSync()
}
I'm trying to add or update all NewsEntity and delete old, in Java i used List.removeAll(Collection<T>) but i can't found how to do this in Swift.
I got an exception when i override equals and hashcode in NewsEntity class.
Before continuing, is it the correct way to do this ?
If yes there is any good tutorial which demonstrate how to do this?
If no what is the correct way ?
Typically Core Data transactions should always be performed on the object's Managed Object Context thread. For this reason you will see the performBlock and performBlockAndWait calls in NSManagedObjectContext.
Since you are using the main thread you are technically synchronous assuming you are making those update calls on the main thread. If you are not then I would suggest wrapping your synch call into a performBlockAndWait call.
That being said, you should leverage Apple's Documentation on the subject as they explain how you can implement multithreaded core data. You should always perform your server related updates on a background thread.
If you want to implement a removeAll feature you will need to manually fetch all the objects you want to remove and call context.deleteObject(managedObject). Alternatively if you want something more powerful that should enforce cascade deletion, you can set this in your model editor when you select the relationship. The following Delete Rules are available:
Nullify
Cascade
No Action
Deny
Finally, you might find this post useful in explaining some of the commonly used Core Data stack setups and the various performance of each.
Welcome to iOS and good luck:)
EDIT
As an aside you might find Ray Wenderlich provides some great Core Data Tutorials
Related
I'm learning to be an iOS app developer and I want to make an app which stores core data. I know how to do it using tables but is there a way I can store data without using tables? Like I'm trying to make an app which saves about a 100 different variables but m not using tables in it. Can someone please direct me to a full tutorial of how it's done? I came across one tutorial on Ray weindervich but it was done on swift 1.2 and it didn't work for me. Thanks
Core data depends on entities, now something that might help to share (probably you knew this already) is that Core data entity is not a table it represents a thing that can be identify and quantify for example a fruit regardless what your back end is; with that been said, now I have a question, when you say table do do you mean the entities or an actual database table? If you mean entity, with Core data you can say use SQLite as backend or xml file as back end but regardless how you store the data you will need to create at least one entity.
What was suggested in the comments still using entities. So my suggestion would be just create one entity using one of this options:
1. Entity
variable1 datatype
variable2 datatype
...
...
variable n datatype
or
2. Entity
key String
Value object
With option one you will have to know all the possible variables that your application will use and one good advantage is that you won't have to do down casting neither unwrapping.
With option two you don't need to know all the possible variables and also your data can grow without changing the app, the only downside is you will have to wrap and unwrap the data from each record.
These are my suggestions for you.
Hope this help
UPDATE :
So here are the steps that I think you need to follow to achieve your request (important: this a simple sample):
Make sure your project has enable since creation Core Data.
In the Model add the Entity as the picture shows:
Then add the attributes as the picture shows:
Add the add the subclass; this part is not mandatory but makes it easy to handle each entity and its properties.
Then you should have something similar to the following code for the entity:
import Foundation
import CoreData
class Generic: NSManagedObject {
#NSManaged var key: String?
#NSManaged var value: NSObject?
}
And your view controller should have something like this in order to read and save the data:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet var txtVariable: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context = delegate.managedObjectContext
let request = NSFetchRequest(entityName: "Generic")
let filter = NSPredicate(format: "key = %#", "xyz")
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filter])
do {
let records = try context.executeFetchRequest(request) as! [Generic]
if (records.count>0)
{
txtVariable.text = (records[0].value as! String)
}
}
catch let error as NSError{
NSLog(error.localizedDescription)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func btnSave_Click(sender: AnyObject) {
let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context = delegate.managedObjectContext
let record = NSEntityDescription.insertNewObjectForEntityForName("Generic", inManagedObjectContext: context) as? Generic
record?.key = "xyz"
record?.value = txtVariable.text
do {
try context.save()
}
catch let error as NSError{
NSLog(error.localizedDescription)
}
}}
I'll make it short as possible.
I have an API request that I fetch data from (i.e. Parse).
When I'm getting the results I'm writing it to Realm and then adding them to a UICollectionView's data source.
There are requests that take a bit more time, which run asynchronous. I'm getting the needed results after the data source and collection view was already reloaded.
I'm writing the needed update from the results to my Realm database.
I have read that it's possible to use Realm's Results. But I honestly didn't understood it. I guess there is a dynamic and safe way working with collection views and Realm. Here is my approach for now.
This is how I populate the collection view's data source at the moment:
Declaration
var dataSource = [Realm_item]()
where Realm_item is a Realm Object type.
Looping and Writing
override func viewDidLoad() {
super.viewDidLoad()
for nowResult in FetchedResultsFromAPI
{
let item = Realm_item()
item.item_Title = nowResult["Title"] as! String
item.item_Price = nowResult["Price"] as! String
// Example - Will write it later after the collectionView Done - Async request
GetFileFromImageAndThanWriteRealm(x.image)
// Example - Will write it later after the collectionView Done - Async request
dataSource.append(item)
}
//After finish running over the results *Before writing the image data*
try! self.realm.write {
self.realm.add(self.dataSource)
}
myCollectionView.reloadData()
}
After I write the image to Realm to an already created "object". Will the same Realm Object (with the same primary key) automatically update over in the data source?
What is the right way to update the object from the data source after I wrote the update to same object from the Realm DB?
Update
Model class
class Realm_item: Object {
dynamic var item_ID : String!
dynamic var item_Title : String!
dynamic var item_Price : String!
dynamic var imgPath : String?
override class func primaryKey() -> String {
return "item_ID"
}
}
First I'm checking whether the "object id" exists in the Realm. If it does, I fetch the object from Realm and append it to the data source. If it doesn't exist, I create a new Realm object, write it and than appending it.
Fetching the data from Parse
This happens in the viewDidLoad method and prepares the data source:
var query = PFQuery(className:"Realm_item")
query.limit = 100
query.findObjectsInBackgroundWithBlock { (respond, error) -> Void in
if error == nil
{
for x in respond!
{
if let FetchedItem = self.realm.objectForPrimaryKey(Realm_item.self, key: x.objectId!)
{
self.dataSource.append(FetchedItem)
}
else
{
let item = Realm_item()
item.item_ID = x.objectId
item.item_Title = x["Title"] as! String
item.item_Price = x["Price"] as! String
let file = x["Images"] as! PFFile
RealmHelper().getAndSaveImageFromPFFile(file, named: x.objectId!)
self.dataSource.append(item)
}
}
try! self.realm.write {
self.realm.add(self.dataSource)
}
self.myCollectionView.reloadData()
print(respond?.count)
}
}
Thank you!
You seem to have a few questions and problems here, so I'll do my best.
I suggest you use the Results type as your data source, something like:
var dataSource: Results<Realm_item>?
Then, in your viewDidLoad():
dataSource = realm.objects(Realm_item).
Be sure to use the relevant error checking before using dataSource. We use an optional Results<Realm_item> because the Realm object you're using it from needs to be initialised first. I.e., you'll get something like "Instance member * cannot be used on type *" if you try declaring the results like let dataSource = realm.objects(Realm_item).
The Realm documentation (a very well-written and useful reference to have when you're using Realm as beginner like myself), has this to say about Results...
Results are live, auto-updating views into the underlying data, which means results never have to be re-fetched. Modifying objects that affect the query will be reflected in the results immediately.
Your mileage may vary depending on how you have everything set up. You could try posting your Realm models and Parse-related code for review and comment.
Your last question:
What is the right way to update the "object" from the Data Source after i wrote the update to same object from the Realm DB?
I gather you're asking the best way to update your UI (CollectionView) when the underlying data has been updated? If so...
You can subscribe to Realm notifications to know when Realm data is updated, indicating when your app’s UI should be refreshed for example, without having to re-fetch your Results.
I've seen many tutorials and they really help me with understand parent-child managed object context and other things related to this. I am ready to start using it in my app but I have a question. Why nobody use singleton for keeping main managed object context. I guess it would be much better to extract Core Data related objects from AppDelegate and set it to own class right? Something like in this Tutorial at raywenderlich.com. But they still instantiate CoreDataStack class (no problem with this, singleton must be instantiate too) and when it's need they set managedObjectContext in prepareForSegue (and set it to first view controller from AppDelegate). Why not to remove this need and just use singleton CoreDataStack and have possible to use managedObjectContext in each controller if needed?
Second and bonus question: I think it's better to have less code in controller and more in other classes. I think it helps with readability. So what if I move this code from controller and set it for example to CoreDataStack class or some other class that helps with Core Data requests and responses:
func surfJournalFetchRequest() -> NSFetchRequest {
let fetchRequest =
NSFetchRequest(entityName: "JournalEntry")
fetchRequest.fetchBatchSize = 20
let sortDescriptor =
NSSortDescriptor(key: "date", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
I know it's possible but is it better? If you get app codes from me would it be better if in controller it would be one line CoreDataStack.fetchRequest("JournalEntry", sortedKey: "date")?
And what about if I take this code and insert it to singleton and created function with closure? I would created child managed context in singleton and do needed operations in there and in controller I would just changed UI:
func exportCSVFile() {
navigationItem.leftBarButtonItem = activityIndicatorBarButtonItem()
let privateContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateContext.persistentStoreCoordinator = coreDataStack.context.persistentStoreCoordinator
privateContext.performBlock { () -> Void in
var fetchRequestError:NSError? = nil
let results = privateContext.executeFetchRequest(self.surfJournalFetchRequest(), error: &fetchRequestError)
if results == nil {
println("ERROR: \(fetchRequestError)")
}
let exportFilePath = NSTemporaryDirectory() + "export.csv"
let exportFileURL = NSURL(fileURLWithPath: exportFilePath)!
NSFileManager.defaultManager().createFileAtPath(exportFilePath, contents: NSData(), attributes: nil)
var fileHandleError: NSError? = nil
let fileHandle = NSFileHandle(forWritingToURL: exportFileURL, error: &fileHandleError)
if let fileHandle = fileHandle {
for object in results! {
let journalEntry = object as! JournalEntry
fileHandle.seekToEndOfFile()
let csvData = journalEntry.csv().dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
fileHandle.writeData(csvData!)
}
fileHandle.closeFile()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
println("Export Path: \(exportFilePath)")
self.showExportFinishedAlertView(exportFilePath)
})
} else {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.navigationItem.leftBarButtonItem = self.exportBarButtonItem()
println("ERROR: \(fileHandleError)")
})
}
}
}
I just want to be sure that my aproach would be okay and would be better than original. Thanks
I built my first core data app with a singleton pattern. It seemed logical for me because there is only one core data stack anyway. I was very wrong, the singleton pattern turned into a big mess quickly. I added more and more code to bend the singleton stack to something that works. In the end I gave up and I invested the time to replace the singleton mess with dependency injection.
Here are some of the problems I encountered before I dumped the singleton:
Since the app kept important data, my users requested a backup functionality. To restore from a backup I switched the sqlite file and then I would just create a new Core Data stack. Doing this in a clean way is next to impossible if you use a pull-pattern to get the managedObjectContext from a singleton. So my way to switch the Core Data stack was to tell the user that they have to restart the app. Followed by an exit(). Not the most elegant way to handle this.
After Apple added childContexts I decided to get rid of undo managers and context rollbacks, because that never worked 100% for me. But changing my editing viewControllers so they use child contexts which are discarded when the user hits cancel, was an incredible painful act because I now had a mix of singleton contexts and viewController local contexts in one viewController.
For editing the targets of relationships I had editViewControllers inside editViewController. Because I created the edit context inside the edit viewControllers I ended up saving data to the main context that shouldn't have been saved. It's a bit complicated to explain, but the second viewController saved stuff like new objects to the main context even if the user in the outer edit viewController hit cancel. Which always lead to orphaned objects. So I added more code to bend the singleton in a way that would make it less of a singleton.
I also had a CSV import function and I wanted to preview the data to the user before they press "Import". I build a totally new infrastructure for that. First I parsed the CSV into a data structure that basically duplicated my core data classes. Then I build a viewController to display these non core data classes, with even more code duplication. I would only start to create core data objects when the user pressed import.
After I got rid of the singleton pattern I could reuse the existing data display viewController. I would just give it a different context, in this case an in-memory context that contained the data that will be imported. Much cleaner, less duplicated code.
I guess some of these problems were not really the singletons fault. I was just very inexperienced.
But I still would strongly recommend against singleton core data.
would be one line CoreDataStack.fetchRequest("JournalEntry", sortedKey: "date")?
You don't need a singleton for this. Stuff like this should be in the NSManagedObject subclass you create for JournalEntry.
And what about if I take this code and insert it to singleton and created function with closure? I would created child managed context in singleton and do needed operations in there and in controller I would just changed UI:
And why don't you create a method that doesn't require internal state at all?
class func export(#context: NSManagedObjectContext, toCSVAtPath path: String,
progress: ((current: Int, totalCount: Int) -> Void)?,
completion: ((success: Bool, error: NSError?) -> Void)?) {
Much more flexible.
This question seems to have been asked a lot already. But I havn't been able to find a swift solution.
I have an entity named "User" which there should only ever be one of. So after it has been created for the first time. I need to edit/update it's values rather than creating more.
I did find a video on it on youtube. Which is the attempt provided below. However it doesn't work.
This is my code in the viewController Class before viewDidLoad()
// Core data setup
let context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var newUser : User? = nil
var results = NSArray()
var hasAccount = Bool()
Here's my code in viewDidLoad()
println(newUser)
// Core data fetching
var request = NSFetchRequest(entityName: "User")
request.returnsObjectsAsFaults = false
results = context.executeFetchRequest(request, error: nil)!
if results.count > 0 {
hasAccount = true
var res = results[0] as NSManagedObject
println(res.valueForKeyPath("email")!)
}
else {
println("No Account")
}
I use the above code, to figure out if there is already something in the User entity, (then setting hasAccount to "true"). However, if you happen to know a better way to do this. it would be very appreciated!
I printed out "newUser" in hopes that I could make it print the User object. So I could just check if newUser != nil { do this } However all my attempts of getting "User" outside of the above workaround has failed.
Now the code that I havn't been able to get to work in any way is this:
let ent = NSEntityDescription.entityForName("User", inManagedObjectContext: context)
if hasAccount == false {
println("Creating a new User Object")
let newUser = User(entity: ent!, insertIntoManagedObjectContext: context)
newUser.firstName = firstNameTextField.text
newUser.lastName = lastNameTextField.text
newUser.email = emailTextField.text
newUser.password = passwordTextField.text
newUser.accountType = 0
context.save(nil)
} else {
println("Updating existing User Object")
newUser?.firstName = firstNameTextField.text
newUser?.lastName = lastNameTextField.text
newUser?.email = emailTextField.text
newUser?.password = passwordTextField.text
newUser?.accountType = 0
context.save(nil)
}
println(newUser)
The part that's creating a new User object. Should work. (However untested since I moved it into an if statement)
But the part that's supposed to update the entity, doesn't work. I know it runs due to the println.
But it doesn't change the User Entity.
Any help would be greatly appreciated!
Let me start by saying that Core Data is an incredibly powerful framework for persisting and maintaining object graphs. That being said it does, unfortunately, require a large amount of effort just to get started. For this particular reason I typically recommend to beginners looking at MagicalRecord. It's a delightful library that removes almost all of the boiler plate code required setting up the stack and maintaining and creating contexts.
Mattt Thompson (I do miss his writing) wrote an insightful post regarding when you should consider CoreData versus NSKeyedArchiver etc. Always handy to keep for reference.
I broke my solution up into two parts, in the first part I reviewed your original question just to give you some basic pointers. The last part I deal with MagicalRecord as I really feel that it might be better suited for you given its lighter learning curve. We are here to ship apps after all, learn Core Data as you get deeper into iOS.
Question Review
Before I dive into things I want to explore some of the mistakes we can rectify with your solution so you can understand why its not working.
// Core data setup
let context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
I know Apple throw the context into the AppDelegate but its a really messy hack and I typically find its best to have a CoreData singleton somewhere that wraps your contexts. Its nice to stay true to the singular responsibility principle, the App Delegate is responsible for handling App Events and instantiating the ViewController hierarchy. NOT for managing the core data stack.
var newUser : User? = nil
var results = NSArray()
This is swift, why not var results:[AnyObject] = []
var hasAccount = Bool()
you mean, var hasAccount = false....
But lets fast forward, you don't need to define all these variables upfront, anyone reading your code has to scan so many lines before they actually get to the root of what you are trying to achieve. Lets clean it up:
// Core data fetching
let userRequest = NSFetchRequest(entityName: "User")
userRequest.fetchLimit = 1
if let user = context.executeFetchRequest(request, error: nil)?.first as? User {
//now we have a user
} else {
//no user
}
One of the big gotchas that newcomers don't realize is that an NSManagedObjectContext save operation only saves one level up. If that context descends directly from the persistent store then your changes will be saved to disk but if it isn't, that save needs to recursively call save on that contexts parent etc. Here's the documentation:
If a context’s parent store is a persistent store coordinator, then
changes are committed to the external store. If a context’s parent
store is another managed object context, then save: only updates
managed objects in that parent store. To commit changes to the
external store, you must save changes in the chain of contexts up to
and including the context whose parent is the persistent store
coordinator.
So if your context has a parentContext then your save operation will never be saving the user to disk:)
Enter MagicalRecord
Getting started with Magical Record is incredibly simple:
MagicalRecord.setupCoreDataStackWithAutoMigratingSqliteStoreNamed(MyCoreDataStoreName)
This typically goes in your App Delegate. They also have tons of amazing categories on most Core Data classes for common Core Data operations as well as some well thought out infrastructure for handling multi-threading issues.
So now you want to establish a record in your database that there should only be one. Before you get started with that you must make sure that there is also only one object responsible for accessing this user attribute, or in the very least you make sure that only one object can create this user attribute.
For simplicities sake lets call this class the UserManager. The user manager will be responsible for managing all the operations we want to perform with our user. For now it just needs to make sure that when we access it, that there is always one in the database.
class UserManager {
class var sharedManager : UserManager {
struct Static {
static let instance : UserManager = UserManager()
}
return Static.instance
}
func currentUser() -> CurrentUser {
if let user = CurrentUser.MR_findFirst() as? CurrentUser {
return user
} else {
let user = CurrentUser.MR_createEntity() as CurrentUser
user.managedObjectContext!.MR_saveToPersistentStoreAndWait()
return user
}
}
}
Now we have a singleton that will always guarantee that we have one user in the database. You also mentioned that you want to be able to update your user, a naive implementation could be added to our UserManager:
func updateUser(userUpdateHandler: ((user: CurrentUser) -> Void)) {
MagicalRecord.saveWithBlock { (context) -> Void in
let user = self.currentUser()
userUpdateHandler(user: user)
//as the app grows you could probably post a notification
//here so any interested parties could update their info
//if the user changes....
}
}
Calling this from you View Controller is trivial:
//the updateUser function isn't called
//on the main thread so we need to capture
//the values from the UI so we can safely use
//them in the background
let firstName = firstNameTextField.text
let lastName = lastNameTextField.text
let email = emailTextField.text
let password = passwordTextField.text
UserManager.sharedManager.updateUser { (user) -> Void in
user.firstName = firstName
user.lastName = lastName
user.email = email
user.password = password
user.accountType = 0
}
And voila, you have now implemented a pretty standard set of functions to deal with a user in your app.
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'?