Core Data and NSOperation - core-data

I'm currently working with an NSPersistentDocument subclass that uses NSOperation to import data in the background. As per the documentation, I'm observing the NSManagedObjectContextDidSaveNotification after saving in the background task and propagating the notification to the NSManagedObjectContext in the main thread using -mergeChangesFromContextDidSaveNotification:.
Everything works fine, but it presents a weird workflow for a user who's importing data into a new document. They need to save an empty document before doing the import (otherwise the -save: fails because the document hasn't configured a URL for the NSPersistentStoreCoordinator.) I don't see a way around this other than some kind of "new document setup" wizard that ensures -writeToURL:ofType:forSaveOperation:originalContentsURL:error: gets called before the import.
Also, it appears that an import task in the background precludes the use of an NSUndoManager on the main thread. (I'm assuming that it's unsafe to share the managed object context's undo manager across the threads.) From a user's point-of-view, there's no way to undo all the new objects created during the import.
I've read both the Core Data Programming Guide and Marcus Zarra's book, but I'm still new to this aspect of the framework. Hopefully, I've overlooked something: if not, I'll adapt my app to these restrictions (the benefits of Core Data far outweigh these user interface limitations.)
Thanks for your time!
--
Based on Peter Hosey's suggestion below, I added the following code to create a temporary persistent store prior to the import:
NSPersistentStoreCoordinator *persistentStoreCoordinator = [self.managedObjectContext persistentStoreCoordinator];
if ([[persistentStoreCoordinator persistentStores] count] == 0) {
// create an in-memory store to use temporarily
NSError *error;
NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
if (! persistentStore) {
NSLog(#"error = %#", error); // TODO: better error handling
}
}
Then, after a file is selected in the save panel, the temporary persistent store is migrated to a SQLite store at the selected URL:
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error
{
NSPersistentStoreCoordinator *persistentStoreCoordinator = [self.managedObjectContext persistentStoreCoordinator];
for (NSPersistentStore *persistentStore in [persistentStoreCoordinator persistentStores]) {
if (persistentStore.type == NSInMemoryStoreType) {
// migrate the in-memory store to a SQLite store
NSError *error;
NSPersistentStore *newPersistentStore = [persistentStoreCoordinator migratePersistentStore:persistentStore toURL:absoluteURL options:nil withType:NSSQLiteStoreType error:&error];
if (! newPersistentStore) {
NSLog(#"error = %#", error); // TODO: better error handling
}
}
}
return [super writeToURL:absoluteURL ofType:typeName forSaveOperation:saveOperation originalContentsURL:absoluteOriginalContentsURL error:error];
}

I'm nobody's Core Data expert, but from what I can tell from the docs, you'll want to start with an in-memory store until the user (in their own time) saves the document. Then, send the coordinator a migratePersistentStore:toURL:options:withType:error: message to change over from the in-memory store to the new truly-persistent store. See that document for some essential details (particularly regarding the fate of the store you migrate).

My first thought on the workflow/saving part would be, if a persistent store hasn't yet been created for the document, to create a temporary in-memory store, so that the imported data would be saved to that store instead (though the document/window would still be marked as dirty). Then, once the user saves the document for real, you would reconfigure the coordinator to remove the in-memory store and replace it with the on-disk store, so all further saves would go to disk.

I'm not 100% familiar with the Mac stuff, but I'm sure you could you use an in-memory persistent store before the user has saved, and then add the sql/plist store after that action.
Potentially even better could be to create a on-disk persistent store in a standard temporary directory, and move it across when the user clicks to save.

Have you tried setting up a temporary file URL when setting up the coordinator?
You should be able to undo the -mergeChangesFromContextDidSaveNotification: on the main thread. No need to register an undo manager for the MOC on the background thread.

Related

Can I use NSBatchDeleteRequest on entities with relationships that have delete rules?

I'm attempting to use NSBatchDeleteRequest to delete a pile of entities, many of these entities have delete cascade and/or nullify rules.
My first attempt to delete anything fails and the NSError I get back includes the string "Delete rule is not supported for batch deletes". I had thought it was fine to delete such things but i was responsible for making sure all the constraints are satisfied before I do a save.
Should I be able to batch delete these managed objects? (I want to keep the delete rules, other delete paths don't have an easy way to know what set of objects to delete) Do some kinds of batch deletes work in this case, but others not? (say predicates fail, but a list of object IDs work?)
Batch delete is problematic with relationships.
It goes directly to the database and deletes the records suspending all object graph rules, including the delete rules. You have correctly identified the requirement that you need to do all the constraint checking yourself again. (That by itself could be a deal-breaker.)
Even if you manage to delete the entities and all the necessary related entities correctly, you will still be left with lots of entries in the (opaque) join table Core Data creates in the background. There is no obvious safe way to delete the entries in the join tables and they have been reported to interfere with managing relationships in future operations.
IMO , the solution in this case is to still use the object graph rather than batch delete and optimize for performance. There are many good answers on SOF on how to do this, but most of it can be summarized with these points:
find the right batch size for saving (typically 500 entities for creation, about 2000 for deletion, but this could vary according to object size and relationship complexity - you have to experiment).
if you have memory constraints, use autoreleasepools.
use a background context to free the UI for interaction. I prefer to do the saving to the database in the background after updating the UI.
I just wrote a simple Department-Employee (one-to-many) demo project. The delete rule of Empolyee's department relationship is set to cascade.
When using batch deletes to delete a department with two employees, the number of deleted objects is only 1. So for the time being, batch deletes disregard delete rules.
You can try it for your self:
func deleteDepartment(named name: String) {
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Department")
fetch.predicate = NSPredicate(format: "name = %#", name)
let req = NSBatchDeleteRequest(fetchRequest: fetch)
req.resultType = .resultTypeCount
do {
let result = try self.persistentContainer.viewContext.execute(req)
as? NSBatchDeleteResult
print(result?.result as! Int) // number of objects deleted
} catch {
fatalError("Error!!!!")
}
}
If anyone would need this:
You can use two NSBatchDeleteRequest for parent and child entities.
let childFetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "ChildEntityName")
let childDeleteRequest = NSBatchDeleteRequest(fetchRequest: childFetchRequest)
do {
try persistenceService.context().execute(childDeleteRequest)
let parentFetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "ParentEntityName")
let parentDeleteRequest = NSBatchDeleteRequest(fetchRequest: parentFetchRequest)
do {
try persistenceService.context().execute(parentDeleteRequest)
persistenceService.saveContext()
/// handle success
} catch {
persistenceService.context().reset() // for example
/// handle error
}
}catch {
/// handle error
}

Proper use of iCloud fallback stores

I am working on a lightweight wrapper for UIManagedDocument which manages the existence of multiple documents in a person's iCloud account. See: APManagedDocument
I am relying heavily on the use of fallback stores in iOS 7 and caring as little as possible about the current state of iCloud. Thus letting core data do what it does best in regards the fallback stores based on my understanding from the WWDC 2013 Video - Session 207 What’s New in Core Data and iCloud
First a quick overview of how my manager works:
I create all my UIManagedDocuments in the local sandbox and set the appropriate persistent store options to enable iCloud sync. I never move the UIManagedDocument package from the sandbox.
When I want to know what documents exist in the cloud I perform a metadata query.
When I want to open one of those documents I check to see if it exists in the local sandbox first and if not create it in the local sandbox.
(This requires that the app needs to wait for the notification corresponding to the Using local storage: 0 message.)
With this set up I never need to know if iCloud is enabled or logged in. I just work locally and let core data do its thing with iCloud.
So far everything is working great but I ran into a little pickle with the scenario where the user creates a new document prior to logging into iCloud and I am presented with the following issues:
I cannot perform a metadata query because there is no iCloud to query.
Because of 1 I have to fall back to doing an intelligent local scan looking for packages that have 'local/store/persistentStore' in their path and listing those as valid documents.
Later when the user logs in it was my understanding that core data would move my local store data to the cloud but I am not seeing that. What I am seeing instead is that a new persistent store is created for the iCloud account and no data.
My big question is what is the proper approach when it comes to the local fallback store? Where are my assumptions wrong?
This is one of the final pieces I need to ship my iOS 7 update. Any and all feedback would be greatly appreciated, and will be reflected in my github project so others can learn from my mistakes.
I have a duplicate of this question in the Apple Developer Forums. I will update this thread with any findings I get from there. I think this question is important and remains unresolved with the release of iOS 7. Fallback stores are a huge advancement in iCloud technology but the local storage part is still a little undefined.
I have worked around this for now since I cannot seem to get information as to how fallback stores are supposed to work in this scenario.
Basically what I do now is if the user is not logged in I create the document w/o iCloud sync options enabled.
Then at startup if iCloud is enabled I perform a scan for documents that need to be migrated and migrate them simply by opening them with the iCloud options enabled. Once opened I close the document as that is enough to get them migrated and scannable via a meta data scan.
Finally after the migration is done I kick off a new scan for documents.
It works but it is a bit of a hack.
Refer to the APManagedDocument commit:
421aaae
I finally got a reply tonight. I am not 100% certain he understood my question, but I am going to spend some time understanding his answer before I render a judgement.
Here is his response:
Thank you for your inquiry to Apple Worldwide Developer Technical
Support. I am responding to let you know that I have received your
request for technical assistance.
Core Data will not automatically move your UIManagedDocument to the
cloud for you. You need to create a new document in the ubiquity
container then migrate the persistent store from your local sandbox to
your ubiquity container. The migration is necessary to create all the
transaction logs so that other devices can create that document.
You could implement this class method to your UIManagedDocument
subclass:
(void)moveDocumentAtURL:(NSURL *)sourceDocumentURL toUbiquityContainer:(NSURL *)ubiquityContainerURL;
That method would essentially create a new document at
"ubiquityContainerURL", and you migrate the store from
"sourceDocumentURL" to "ubiquityContainerURL". You would use
"migratePersistentStore" to perform the migration.
Here's an example:
// The name of the file that contains the store identifier.
static NSString *DocumentMetadataFileName = #"DocumentMetadata.plist";
// The name of the file package subdirectory that contains the Core Data store when local.
static NSString *StoreDirectoryComponentLocal = #"StoreContent";
// The name of the file package subdirectory that contains the Core Data store when in the cloud. The Core Data store itself should not be synced directly, so it is placed in a .nosync directory.
static NSString *StoreDirectoryComponentCloud = #"StoreContent.nosync";
+ (NSDictionary *)optionsForStoreAtURL:(NSURL *)url {
NSURL *metadataDictionaryURL = [url URLByAppendingPathComponent:DocumentMetadataFileName];
NSDictionary __block *storeMetadata = nil;
/*
Perform a coordinated read of the store metadata file; the coordinated read ensures it is downloaded in the event that the document is cloud-based.
*/
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fileCoordinator coordinateReadingItemAtURL:metadataDictionaryURL options:0 error:NULL byAccessor:^(NSURL *newURL) {
storeMetadata = [[NSDictionary alloc] initWithContentsOfURL:newURL];
}];
NSString *persistentStoreUbiquitousContentName = nil;
if (storeMetadata != nil) {
persistentStoreUbiquitousContentName = [storeMetadata objectForKey:PersistentStoreUbiquitousContentNameKey];
if (persistentStoreUbiquitousContentName == nil) {
// Should not get here.
NSLog(#"ERROR in optionsForStoreAtURL:");
NSLog(#"persistentStoreUbiquitousContentName == nil");
abort();
}
}
else {
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidString = CFUUIDCreateString(NULL, uuid);
persistentStoreUbiquitousContentName = (__bridge_transfer NSString *)uuidString;
CFRelease(uuid);
}
// NSPersistentStoreUbiquitousContentURLKey should be the TransactionLogs directory.
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
persistentStoreUbiquitousContentName, NSPersistentStoreUbiquitousContentNameKey,
[[self URLForUbiquityTransactionLogs] URLByAppendingPathComponent:persistentStoreUbiquitousContentName] , NSPersistentStoreUbiquitousContentURLKey, nil];
return options;
}
+ (void)moveDocumentAtURL:(NSURL *)sourceDocumentURL toUbiquityContainer:(NSURL *)ubiquityContainerURL {
if (ubiquityContainerURL == nil) {
// iCloud isn't configured.
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
NSLocalizedString(#"iCloud does not appear to be configured.", #""), NSLocalizedFailureReasonErrorKey, nil];
NSError *error = [NSError errorWithDomain:#"Application" code:404 userInfo:dict];
NSLog(#"%#", [error localizedFailureReason]);
return;
}
// Move the document to the cloud using its existing filename
NSManagedObjectModel *model = [self managedObjectModel];
NSDictionary *ubiquitousOptions = [self optionsForStoreAtURL:sourceDocumentURL];
NSString *documentName = [[sourceDocumentURL lastPathComponent] stringByDeletingPathExtension];
documentName = [documentName stringByAppendingPathExtension:#"wwWhat"];
NSURL *destinationURL = [ubiquityContainerURL URLByAppendingPathComponent:documentName];
dispatch_queue_t q_default;
q_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(q_default, ^{
NSError __block *error = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
[coordinator coordinateWritingItemAtURL:destinationURL options:NSFileCoordinatorWritingForReplacing error:nil byAccessor:^(NSURL *destination) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtURL:destination error:nil];
NSURL *destinationStoreDirectoryURL = [destination URLByAppendingPathComponent:StoreDirectoryComponentCloud isDirectory:YES];
NSURL *destinationStoreURL = [destinationStoreDirectoryURL URLByAppendingPathComponent:StoreFileName isDirectory:NO];
NSURL *sourceStoreURL = [[sourceDocumentURL URLByAppendingPathComponent:StoreDirectoryComponentLocal isDirectory:YES] URLByAppendingPathComponent:StoreFileName isDirectory:NO];
NSURL *originalMetadataURL = [sourceDocumentURL URLByAppendingPathComponent:DocumentMetadataFileName isDirectory:NO];
NSURL *destinationMetadataURL = [destination URLByAppendingPathComponent:DocumentMetadataFileName isDirectory:NO];
[fileManager createDirectoryAtURL:destinationStoreDirectoryURL withIntermediateDirectories:YES attributes:nil error:nil];
[fileManager copyItemAtURL:originalMetadataURL toURL:destinationMetadataURL error:nil];
NSPersistentStoreCoordinator *pscForSave = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
id store = [pscForSave addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sourceStoreURL options:nil error:nil];
id success = [pscForSave migratePersistentStore:store toURL:destinationStoreURL options:ubiquitousOptions withType:NSSQLiteStoreType error:&error];
if (success) {
[fileManager removeItemAtURL:sourceDocumentURL error:NULL];
}
else {
NSLog(#"Failed to migrate store: %#", error);
}
}];
});
}
Isn't his response consistent with what you are seeing, I.e. No data because you don't put the transaction logs out in the cloud so that other devices can recreate the document from the logs. By migrating I guess you have the log files automatically generated in the appropriate iCloud directory. But then the 207 video seems to indicate that any use of .sync folder is no longer required.
One good working example from them is all I ask for...
Any idea how one would access these files from OSX?
Btw your stuff looks pretty good, I hope to try using it in a couple of days. Am real keen to see how these files would be accessed from OSX. As I understand it NSPersistentDocument is not iCloud aware.
EDIT:
I just had a closer look at your APManagedDocumentManager and you don't seem to be including the iCloud path in the NSPersistentStoreUbiquitousContentURLKey value. Unless I missed something you are just using the subdirectory not the full iCloud path, also you're using a NSString rather than a URL (not sure whether this makes a difference).
EDIT: We should have a discussion over the phone perhaps? Anyway some more of my findings below:
I just installed Mavericks and after having gone over the video twice am testing the following:
Create new files using only the code below - no UIManagedDocument or anything. _storeURL is pointing to the local directory as suggested in the video. And I am not using any NSPersistentStoreUbiquitousContentURLKey because its not longer necessary. At the moment my filename has no UUID.
When I do this on any device then a CoreData directory is created outside the iCloud Documents directory. Inside the CoreData directory are subdirectories for each of the fileNames, and inside these are various zip files and things which are presumably baseline and log files. No sign of any DocumentMetaData.plist. So this all looks quite promising, except I can't figure out how one would "Discover" new files that appear. I am hoping that I just need to register for some notifications and am done... Going back to the video now because I can't recall the detail on what notifications are sent and how to react to them. At least the behaviour is consistent on both platforms. Strangely enough none of these documents show up in the Mac Apps File-Open dialog which lists all documents in the iCloud Documents directory, well not so strange I guess.
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
//FLOG(#" got_persistentStoreCoordinator is %#", _persistentStoreCoordinator);
FLOG(#" calling addPersistentStoreWithType for path %#", [_storeURL path]);
NSString *fileName = [[_storeURL URLByDeletingPathExtension] lastPathComponent];
FLOG(#" setting NSPersistent for path %#", [_storeURL path]);
#try {
store = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:_storeURL
options:#{NSPersistentStoreUbiquitousContentNameKey:fileName,
NSMigratePersistentStoresAutomaticallyOption:#YES,
NSInferMappingModelAutomaticallyOption:#YES,
NSSQLitePragmasOption:#{ #"journal_mode" : #"DELETE" }}
error:&error];
...

Is it possible to have multiple core data "databases" on one iOS app?

I'm wanting to write a "management" game that utilizes Core data heavily. The game requires a pre-set, pre-defined dataset that cannot be changed by the user/system; it is used to seed the game with data and is meant to be read-only.
The best example I can give is a football management game, but it could be anything. In some football management sims they give you scenarios and pre-set datasets.
As the user proceeds through the game they can save/load their progress which is saved to the core data.
In addition to this, the user can receive updates to the pre-defined data or can purchase scenarios packs of data; which is saved to their device.
So, there could be multiple "core data databases" (yes, I know core data isn't strictly a database) or "buckets" which the app can dive into and use.
The schema of the data would not change.
So we have:
Pre-defined data (Default data) that is only used for seeding the game.
The user's current save game.
The user has downloaded a scenario from the Internet.
Problem: What happens when the user saves the game whilst on a "scenario".
Problem: How do I keep track of all the scenarios and all the user saved games in core data?
This sounds like multiple databases at a given time. Obviousily one should restrict how many save games a user can make.
An alternative solution to this is that the user's device exports a back-up copy of the data in JSON or XML and this serves as the "save data" and I could use this strategy for scenarios too. Obviousily some kind of encryption would be needed to prevent people simply changing stats in the game via the XML.
But I'm wondering from the outset what would be the best way to use Core data for iOS devices handle more than 1 core data "database"?
Thanks for your time
If the data models are the same, you can just setup your MOC so that it uses both persistent stores... one which is read-only and the other that is read/write.
Or, you could use separate MOC for each store.
So, how you want to use it is your only decision factor, since you can have almost any combination of MOC/PSC.
Look at the documentation here for more information.
Edit:
The link given with this question is dead, someone else suggested this link in another deleted answer.
NB: This is an old question, but the problems it describes are timeless, so I've written the answer as if the question were posted today.
Actually, none of this suggests the need for multiple databases. So we have:
1) Pre-defined data (Default data) that is only used for seeding the
game.
Write a method that loads the data into the persistent store (database). Set a flag in user default, defaultDataHasBeenLoaded or something like that, and check that in the appDelegata.
2) The user's current save game.
You need a Users table and a Games table with a one-to-many relationship. In the Games table you add an isCurrentGame attribute.
3) The user has downloaded a scenario from the Internet.
Now it's getting interesting. You will need an import function or class for that and you'll want to run that on a background thread. That way, your user can continue playing, or looking looking at their scores or whatever, while the new scenario is being imported. When the scenario has been imported, the user should get a notification and the opportunity to switch to the new scenario.
The most efficiënt way to do this is to use NSPeristentContainer which is available from iOS 10.0, macOS 10.12, tvOS 10.0 and watchOS 3.0. Give NSPeristentContainer the name of the data model and it will create or load a persistent store and set the persistentStoreCoördinator and the managedObjectContext.
// AppDelegate.h or class header file
#property (readonly, strong, nonatomic) NSPersistentContainer *persistentContainer;
#property (readonly, weak, nonatomic) NSManagedObjectContext *managedObjectContext;
// AppDelegate.m or other implementation file
#synthesize persistentContainer = _ persistentContainer;
#synthesize managedObjectContext = _ managedObjectContext;
- (NSPersistentContainer *)persistentContainer
{
#synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:#"nameOfDataModel"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
// Handle the error
} else {
_managedObjectContext = _persistentContainer.viewContext; // NB new name for moc is viewContext!
}
}];
}
}
return _persistentContainer;
}
To use the the container from the appDelegate in an NSViewController, you add the following to viewDidLoad:
self.representedObject = [(AppDelegate *)[[NSApplication sharedApplication] delegate] persistentContainer];
// Use representedObject in bindings, such as:
[_gameNameTextField bind:NSValueBinding toObject:self
withKeyPath:#"representedObject.game.name"
options:options];
To import the new scenario, use performBackgroundTask:, a block which will automatically create a new thread and a new managedObjectContext (here called moc_background). Use only moc_background for anything you do in the block -- if you call a method outside the block, pass it moc_background.
NSPersistentContainer *pc = (NSPersistentContainer *)self.representedObject;
pc.viewContext.automaticallyMergesChangesFromParent = YES; // this will ensure the main context will updated automatically
__block id newScenario;
[pc performBackgroundTask:^(NSManagedObjectContext * _Nonnull moc_background) {
NSEntityDescription *scenarioDesc = [NSEntityDescription entityForName:#"Scenario" inManagedObjectContext:moc_background];
NSManagedObject *scenario = [[NSManagedObject alloc] initWithEntity:scenarioDesc insertIntoManagedObjectContext:moc_background];
// configure scenario with the data from newScenario
NSError *error;
BOOL saved = [moc_background save:&error];
// send out a notification to let the rest of the app know whether the import was successfull
}];
Problem: What happens when the user saves the game whilst on a
"scenario".
That depends on who gets there first, the background thread that attempts to merge or the save operation. If you add a Scenario table with many-to-one relationship to the Game table, there should not be any problems.
Problem: How do I keep track of all the scenarios and all the user
saved games in core data?
Data modeling can be tricky. Keep it simple at first and add tables and relationships when you find a clear need for them. And then test, test, test.

Persist data between views

I am developing an app in which the user enter a search word and receives results from a web service (I control the web service). The results are displayed on a uitable view and then the user can select a row and navigate to three - four level of inner details (I am using the uinavigation control).
I am wondering what is the best way to persist the data between the views.
I am currently using the application delegate to store an array of objects which I can access from everywhere in the app. It works fine but I read that it is not a good practice for the job (I am concerned about memory issues) . I tried using Core Data framework for the job but than I realized that I would have to read my web service results, and loop them one by one in order to insert them to database. I also will have to delete old data because the data I am saving is only actual for the current search. I read about p-lists and saving data to file system but could not find a real good solution for my job...
Any help will be appreciated!!! Thanks!!!
You can use JSON or PLISt for communication, although binary plist is - according to apple - much faster on the phone side.
Creating a dictionary from the binary Plist is relatively simple:
NSPropertyListFormat format;
NSDictionary *myDictionary = [NSPropertyListSerialization
propertyListFromData:data mutabilityOption:NSPropertyListMutableContainers
format:&format errorDescription:&errorString];
Creating it from a JSON just requires using one of the readily available JSON libraries. Once you have the dictionary, save it:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *file = [NSString stringWithFormat: #"%#%#.plist", [paths objectAtIndex:0], #"MyPlistBaseName"];
[plist writeToFile: file atomically:NO];
And re-load it later:
NSDictionary *plist = [[[NSDictionary alloc] initWithContentsOfFile: file] autorelease];
However, If you encapsulate the access to this data in a singleton, then you can worry about optimizing the implementation if speed / memory becomes an issue. Just treat that signleton as the 'owner' of the data, with methods like count, getGroup(0) (to return a block of 25), etc. Then you can hide all the implementation details inside the object.

How do you correctly update a model in Xcode4 without corrupting it?

I never had any problems with Xcode3, but with Xcode4 I'm getting Apple's code failing approx 1 time in 3 when I update a core data model, with the dreaded "Persistent store migration failed, missing source managed object model." error.
Here's my setup (how I configured the project to auto-migrate):
NSPersistentDocument, from Apple's template
Override Apple's model-loading method, and the ONLY thing I do is to provide the two flags in the storeOptions Dictionary, which turn on auto-migration
-(BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error
{
NSMutableDictionary *newOptions = nil;
if( storeOptions != nil )
newOptions = [NSMutableDictionary dictionaryWithDictionary:storeOptions];
else
newOptions = [NSMutableDictionary dictionary];
[newOptions setValue:#"YES" forKey:NSMigratePersistentStoresAutomaticallyOption];
[newOptions setValue:#"TRUE" forKey:NSInferMappingModelAutomaticallyOption];
BOOL success = FALSE;
success = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:newOptions error:error];
return success;
}
Here's the process I've been using (which is already working around 1 bug in Xcode4!)
Select the model (named "something.xcdatamodel" in Xcode4, with a twisty on the left)
Go to Editor menu, select "Add new model version..."
Name the new version 1 integer higher than last - e.g. if previous was "4" name the new one "5"
In the right-hand pane, change the current model version to the newly-created one
workaround for XCode4 bug: select any file, then select the newly-created model. If you do not, Xcode shows the selection on the newly-created model, but will edit the previous model instead, which definitely corrupts everything in CoreData
Edit your model; in this case, I'm adding a new attribute to an existing entity
Save. Build. Run. ... CRASH.
Except, as I said, approx 2 times in 3 this works correctly. Once it works once, it's (obviously) fine - the lightweight migration is complete, the next save saves in the new model version.
So I'm guessing there's something I'm doing wrong in the above steps, but I've been through the docs 5 or 6 times and can't see anything obvious. Doesn't help that NSPersistentDocument docs are all out of date - but I've done lightweight migration on iPhone lots of times too, so I'm reasonably confident with doing this, and it seems right to me.
Other things I've tried/checked:
- iPhone Core Data Lightweight Migration Cocoa error 134130: Can't find model for source store (nope; only the root xcdatamodel was being included)
Use [NSNumber numberWithBool:YES] not #"YES" or #"TRUE".
Since you have eliminated a corrupt development store as a source of the problem, I suspect the problem lays in Xcode 4.x which is buggy to say the least. A lot of people are reporting similar issues but no two problems seem exactly the same. It is probably a bug/s that only occur with specific data model setups so the problem will be very hard to track down.
You may simply have to abandon automatic migration and create an explicit migration map. It takes longer and introduces complexity into your code but it will always work.
If you have a shipping app and will be dealing with end user data in the wild, you do have a moral and business obligation to take the extra step to protect end user data.
I was getting super-confused but this, and it WASN'T working.. because I was assuming that the method would already HAVE a "store options" dictionary.. I just needed to check for it's existence before i set the aforementioned options…
-(BOOL)configurePersistentStoreCoordinatorForURL: (NSURL*)u
ofType: (NSString*)t
modelConfiguration: (NSString*)c
storeOptions:(NSDictionary*)o
error: (NSError**)e
{
return [super configurePersistentStoreCoordinatorForURL:u
ofType:t
modelConfiguration:c
storeOptions:
o ? [o dictionaryWithValuesForKeys:
#[ NSMigratePersistentStoresAutomaticallyOption, #YES,
NSInferMappingModelAutomaticallyOption, #YES]]
: #{ NSMigratePersistentStoresAutomaticallyOption :#YES,
NSInferMappingModelAutomaticallyOption :#YES}
error:e];
}

Resources