Hard CoreData + iCloud scenario - core-data

i'm having a very hard issue to solve. I've got this scenario:
My app uses CoreData to storing objects, I want to implement iCloud sync between devices... and my app requires an initial populated database.
The first time I launch my app, it's going to populate my database on the cloud and marks to YES some db'fields as "databaseInstalled". These fields are synced in the cloud too.
Now when another device launch the app for the first time, I was hoping to retrieve the field "databaseInstalled" to check whether inject or not some data but it's wrong...
If databaseInstalled is false, we inject data, if databaseInstalled it's true, we wait for iCloud sync.
The problem is that I retrieve the persistentStoreCoordinator asynchronically because of I don't want to block the app that is waiting to download data from iCloud...
So how can I know a priori if i need to populate the database or it has been filled on another device and I've just to download from iCloud the populated one?
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if((__persistentStoreCoordinator != nil)) {
return __persistentStoreCoordinator;
}
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
NSPersistentStoreCoordinator *psc = __persistentStoreCoordinator;
// Set up iCloud in another thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// ** Note: if you adapt this code for your own use, you MUST change this variable:
NSString *iCloudEnabledAppID = #"this is a secret!";
// ** Note: if you adapt this code for your own use, you should change this variable:
NSString *dataFileName = #"you do not have to know.sqlite";
// ** Note: For basic usage you shouldn't need to change anything else
NSString *iCloudDataDirectoryName = #"Data.nosync";
NSString *iCloudLogsDirectoryName = #"Logs";
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localStore = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:dataFileName];
NSURL *iCloud = [fileManager URLForUbiquityContainerIdentifier:nil];
if (iCloud) {
NSLog(#"iCloud is working");
NSURL *iCloudLogsPath = [NSURL fileURLWithPath:[[iCloud path] stringByAppendingPathComponent:iCloudLogsDirectoryName]];
NSLog(#"iCloudEnabledAppID = %#",iCloudEnabledAppID);
NSLog(#"dataFileName = %#", dataFileName);
NSLog(#"iCloudDataDirectoryName = %#", iCloudDataDirectoryName);
NSLog(#"iCloudLogsDirectoryName = %#", iCloudLogsDirectoryName);
NSLog(#"iCloud = %#", iCloud);
NSLog(#"iCloudLogsPath = %#", iCloudLogsPath);
// da rimuovere
//[fileManager removeItemAtURL:iCloudLogsPath error:nil];
#warning to remove
if([fileManager fileExistsAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]] == NO) {
NSError *fileSystemError;
[fileManager createDirectoryAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]
withIntermediateDirectories:YES
attributes:nil
error:&fileSystemError];
if(fileSystemError != nil) {
NSLog(#"Error creating database directory %#", fileSystemError);
}
}
NSString *iCloudData = [[[iCloud path]
stringByAppendingPathComponent:iCloudDataDirectoryName]
stringByAppendingPathComponent:dataFileName];
//[fileManager removeItemAtPath:iCloudData error:nil];
#warning to remove
NSLog(#"iCloudData = %#", iCloudData);
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[options setObject:iCloudEnabledAppID forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudLogsPath forKey:NSPersistentStoreUbiquitousContentURLKey];
[psc lock];
[psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[NSURL fileURLWithPath:iCloudData]
options:options
error:nil];
[psc unlock];
}
else {
NSLog(#"iCloud is NOT working - using a local store");
NSLog(#"Local store: %#", localStore.path);
NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[psc lock];
[psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:localStore
options:options
error:nil];
[psc unlock];
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"iCloud routine completed.");
Setup *install = [[Setup alloc] init];
if([install shouldMigrate]) {
HUD = [[MBProgressHUD alloc] initWithView:self.window.rootViewController.view];
HUD.delegate = self;
HUD.labelText = NSLocalizedString(#"Sincronizzazione del database", nil);
[self.window.rootViewController.view addSubview:HUD];
[HUD showWhileExecuting:#selector(installDatabase) onTarget:install withObject:nil animated:YES];
}
else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"setupCompleted" object:self];
}
//[[NSNotificationCenter defaultCenter] postNotificationName:#"icloudCompleted" object:self userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"setupCompleted" object:self];
});
});
return __persistentStoreCoordinator;
}

You can't know whether or not there's going to be data available in iCloud until you finish syncing with iCloud. That means that you've got two options:
Make the user wait until the sync is done.
Start up with your default database and merge changes from iCloud when possible.
With iCloud, you need some strategy for resolving conflicts between local data and cloud data because you have to deal with the fact that users might change data on more than one device at the same time. Once you have that in place, it seems pretty clear that the second option above is the better one: users get to start using your app right away, and data from the cloud is merged when it's available.

I had exactly same problem.
Check out my question & my answer to it iCloud + CoreData - how to avoid pre-filled data duplication?
Actually it doesn't work 100% ok. If you dare to try it I can explain you how you might make it work 100% correctly (I haven't tried yet, though).
Taking into account that you have a lot of data to pre-populate my solution might now work out for you.

There is no way to determine whether a data store is being opened for the first time. At least not on iCloud Core Data store. Think of it, iCloud should also work off-line – that is, all changes should be buffered when the user is disconnected from the Internet and then uploaded when the connection is restored. There is no way to check whether a data store was initialized without potentially making the user wait for a few minutes (or even indefinitely if the device is off-line) to ask iCloud's copy of the data sore.
To solve this, you'll need to follow these four simple guidelines:
Have a way to de-duplicate pre-populated records.
Have a way to identify pre-populated records and differentiate it from user-entered ones.
Run the de-duplication process every time new transaction records came in from iCloud.
Only seed data records once per device/account combination.
You can read more details here: http://cutecoder.org/programming/seeding-icloud-core-data/

Related

mapping model is NULL - core data migration

I follow this nice tutorial http://mipostel.com/index.php/home/70-core-data-migration-standard-migration-part-2 to do my core data migration.
For some strange reason i always get NULL in the mappingModel in these lines:
NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil
forSourceModel:sourceModel
destinationModel:destinationModel];
(line 191 in the linked code)
I tried to create a very simple derived version of the model, I recreated a mappingModel a 1000 times, made sure that the mapping model file is in the project directory - but this call always returns NULL.
Anybody has an idea what is wrong here?
ps I was just wondering that setting the migration options is called AFTER the mapping Model is used.
NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
NSError *error;
NSDictionary *pscOptions = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:NO], NSInferMappingModelAutomaticallyOption,
nil];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeUrl
options:pscOptions
error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
(lines 123...)
Anyway
Why can't the mapping model be found ?
pss couldn't help holding back :-) this core data migration stuff is much too complicated and difficult compared to doing simple SQL DB migration - wasting soooo much time.
So a BIG THANKS in advance!
I followed that same tutorial and ended up having to manualy open my mapping model by URL
NSString *mappingModelPath = [[NSBundle mainBundle] pathForResource:#"mappingModel10" ofType:#"cdm"];
NSLog(#"mapping model path:%#", mappingModelPath);
NSURL *mappingModelUrl = [NSURL fileURLWithPath:mappingModelPath];
NSMappingModel *mappingModel = [[NSMappingModel alloc] initWithContentsOfURL:mappingModelUrl];
I found the file name for my mapping model by looking in my App's bundle.

CoreData autosaving and not loading all data after autosave

I have an NSPersistentDocument subclass using NSManagedObject subclasses for my data.
When a new document is opened, I do some initializing of data structures (trivial amount of populating fields). What I've noticed is that the Untitled document gets autosaved, and when the application re-opens, that document gets loaded. If the application quits, the user doesn't (by default) get prompted with the save dialog. If the window closes, the user does.
First question:
I want to call up the save dialog when the user quits the application. I don't want this Untitled document hanging around (under normal circumstances). I either want it saved or trashed.
I attempted to fill out:
- (void)applicationWillTerminate:(NSNotification *)aNotification
In order to trigger the document to be saved. Calling save: on the context at this point gives an error. From what I can tell, this is because the user hasn't yet saved the file on their own. In addition, calling [self close]; or [[self windowForSheet] close]; close the window without saving.
How can I force the save dialog to come up? How can I trash the untitled document?
Second question (no, I can't count):
Since when the application starts, there may or may not be an Untitled document to deal with, I'm trying to keep track of the state in another model. I've already found that the initial data (to which I referred earlier) is present when the Untitled document came up. My other model has some metadata, including a success flag/state for the populated data. Once the populated data is all in place and correct, the state indicates as such. Unfortunately, while my populated data is being loaded when the app starts with a pre-existing Untitled document, the metadata class is not.
Please excuse the roughness of the code, at this point, I'm mucking it up until I can see that it's working how I want before I polish it back off:
- (bool) createGameState {
NSEntityDescription* description = [NSEntityDescription entityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:description];
NSError *error = nil;
NSArray *array = [[self managedObjectContext] executeFetchRequest:req error:&error];
[req release];
req = nil;
GameState* result = nil;
if (array) {
NSUInteger count = [array count];
if (!count) {
// Create the new GameState.
DebugLog(#"Creating GameState");
result = [NSEntityDescription insertNewObjectForEntityForName:[GameState name] inManagedObjectContext:[self managedObjectContext]];
[result setIsLoaded:[NSNumber numberWithBool:NO]];
} else {
if (count > 1) {
NSLog(#"WARNING: Potentially Corrupt Game State. found: %lu", count);
}
result = [array objectAtIndex:0];
if ([result isLoaded]) {
[self variantLoaded];
} else {
// In this case, we have an aborted set-up. Since the game isn't
// playable, just refuse to create the GameState. This will
// force the user to create a new game.
return NO;
}
}
} else {
DebugLog(#"error: %#", error);
}
[game setState:result];
return result;
}
Note that array is always present, and count is always zero. No, I'm not explicitly calling save: anywhere. I'm relying on the standard auto-save, or the user performing a save.
EDIT:
I installed the Core Data Editor app. It turns out the issue isn't on saving the data, but on loading it. (Note: Due to another issue, the app saves as binary when instructed to save as XML, which causes much head banging.)
I've broken it down to the simplest code, which should pick up all objects of type GameState in an array. It retrieves none, despite there clearly being objects of the appropriate type in the saved file:
NSManagedObjectContext* moc = [self managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:#"GameState" inManagedObjectContext:moc];
NSFetchRequest* req = [[NSFetchRequest alloc] init];
[req setEntity:entity];
NSError *error = nil;
NSArray *array = [moc executeFetchRequest:req error:&error];
Array is not null, but [array count] is 0.
At this point, I'm guessing it's something simple that I'm overlooking.
Second EDIT:
I added -com.apple.CoreData.SQLDebug 5 and saved as SQLite. The call to executeFetchRequest does not generate any debug logs. I do see the INSERT INTO ZGAMESTATE entry show up in the logs. It seems that executeFetchRequest is not getting passed to the backend.
Third EDIT (this one burns):
I created a new xcode project, using core data (as I had with the other). I copied just this one function (stubbing where necessary) and plopped a call to it in windowControllerDidLoadNib. In this new project, the code above works.
Found the problem.
I errantly was loading objects in Document's - (id) init call. Moved to windowControllerDidLoadNib (which is what I did in the test version) and it worked fine.

iOS Core Data how to properly initialize entity relationships?

I have a one to many relationship in my core data model. I need to create a new entity and save it. The entity has a one to many relationship which generated the following code:
- (void)addRelationshipEvent1:(NSSet *)values;
- (void)removeRelationshipEvent1:(NSSet *)values;
.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
ApplicationRecord *newManagedObject = (ApplicationRecord*)[NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
newManagedObject.startDate = [NSDate date];
newManagedObject.stopDate = [[NSDate date] dateByAddingTimeInterval:120];
//keep adding individual dynamic properties
is it correct to set the -toMany relationship sets to nil initially? Or do I need to initialize an (empty?) set here and assign it? Would I be able to add extra objects later if I set the initial set to nil?
newManagedObject.relationshipEvent1 = nil;
newManagedObject.relationshipEvent2 = nil;
//...
// Save the context.
NSError *error = nil;
if (![context save:&error])
{
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
Alex,
You don't need to intialize your relationships. Just use the supplied accessors or helper functions and Core Data takes care of it. IOW, only worry about the property/relationship when you need to actually use it.
Andrew

Core Data, huge file, slow

I am writing an application, which stores a huge number of very short strings (mostly one to three unicode chars) and lots of relationships. This results in a massive overhead for storing the relationships in the XML format and even more so in the Binary format (which is strange). So if I use XML or Binary, i get huge files and very long save and load times.
The SQLite format is more compact and saves faster (especially in case of small changes), but for some reason the queries using predicates with format "$something BEGINSWITH[c] fieldInMyObject" do not work, and i can't do without them.
Is there anything i can do to reduce the volume of the files and speed up loading and saving (apart from using SQLite directly)?
Best regards,
Timofey.
UPD
Here is the code for saving the data:
‐ (IBAction) saveAction:(id)sender {
NSError *error = nil;
if (![[self managedObjectContext] commitEditing]) {
NSLog(#"%#:%s unable to commit editing before saving", [self class], _cmd);
}
if (![[self managedObjectContext] save:&error]) {
[[NSApplication sharedApplication] presentError:error];
}
}
And here is the code for loading data (both for creating new files and loading existing ones):
- (void) panelReturnedURL:(NSURL *)url {
NSManagedObjectModel *mom = [self managedObjectModel];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
initWithManagedObjectModel:mom];
NSError *error = nil;
if (![persistentStoreCoordinator addPersistentStoreWithType: NSBinaryStoreType
configuration:nil
URL:url
options:nil
error:&error]) {
[NSApp presentError:error];
}
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: persistentStoreCoordinator];
[mainWinController window];
}
And i don't don't save when objects are modified, the context is saved when the application quits or when the user explicitly saves it.
XML and binary can be slow for large files because they have to be read entirely into memory in one chunk in order to work. If you have a lot of data your really need to use an SQLite store.
Your problems with the predicate having nothing to do with the SQLite store. That type of predicate is used routinely. I would suggest posting a seperate question with a layout of your entities and the predicate you want to use:

Setting up basic relationship with Fetch Requests

I am wanting to set up a basic relationship with two entities in Core Data, but the relationship is either not saving, or is not working properly and I'm not sure why.
The two entities are Character and Avatar, its a one-to-one relationship. A character can have 1 avatar. Technically, it should be a "one avatar can be owned by many characters", but I'll deal with that later.
I want to add characters and assign them an avatar.
There are already 10 avatars in Core Data and 1 character, both of which I've verified via the Terminal and SQLite.
The problem is, I'm having troubling "finding an avatar by a name and then saving the relationship to a character".
So far,
I set up a fetch request called: "frqAvatarWithName" where the Predicate has the following structure:
[quote]
name == $AVATAR_NAME
[/quote]
This is so: I can find an avatar with a certain name; and then I can create a relationship with a character.
Issue 1: It gets to execute the query but then never displays how many records there are.
I get a EXC_BAD_ACCESS error in debug mode and I have traced it back to the fetch request template handling -- so, this must be in error or I have done it wrong.
Issue 2: I am not sure if I am even setting up this "basic" relationship up properly.
[code]
// This code is meant to find an avatar with a certain name and then save the relationship
// between a character and said avatar.
// This is my app delegate file for the moment
// All the files are present, and I have deleted/recreated the app various times
-(void)characterMaker
{
NSLog(#"Inside characterMaker...");
NSError *error = nil;
NSManagedObjectContext *context = [self managedObjectContext];
NSManagedObjectModel *model = [self managedObjectModel];
// Find an avatar with a specific name
NSString *nameToFind = #"avt_player_1";
// Use a Fetch request template
NSDictionary *subs = [NSDictionary dictionaryWithObjectsAndKeys:nameToFind, #"AVATAR_NAME", nil];
NSFetchRequest *fetchRequest = [model fetchRequestFromTemplateWithName:#"frqAvatarWithName"
substitutionVariables:subs];
// Set the entity to use
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Avatar"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// Execute the query (it never even reaches this point)
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
// Handle the error
NSLog(#"Error -- %#", [error localizedDescription]);
abort();
}
NSLog(#"Found %# records", [fetchedObjects count]);
// Print out avatar names
for (Avatar *a in fetchedObjects)
{
NSLog(#"Name = %#", [a valueForKey:#"name"]);
}
// This is where I would use `a` and store it in a character entity, and thus create the relationship
[/code]
I gave up on this and did the whole project with the FMDatabase project and SQLite; I've been able to resolve the problem this way.
Thread closed.

Resources