3

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;
}
Progeny
  • 672
  • 1
  • 11
  • 25

3 Answers3

0

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.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • the amount of data from pre-populated database is big, from 1k to 4k objects (records). The second option is how it's now working but I'm having another issue... the app is injecting objects on the first time to populate the database, meanwhile it's merging data with iCloud... and I'm having an Exception (Collection was muted) in my NSFetchedResultsController... still troubling... – Progeny May 10 '12 at 14:23
  • i was forgetting... the main issue with option 2 is that my database is duplicated because two devices are synching local database on the iCloud... merging the local database from iCloud doesn't drop existing records, but duplicate its! – Progeny May 10 '12 at 14:28
  • You answer deals with Document Based app and not CoreData + iCloud. There's no simple way to resolve pre-populated db scenario. At least I haven't found one yet. The main problem is that you will have duplicates once you pre-populate DB with data. ObjectId's will be different. – Kostiantyn Sokolinskyi May 11 '12 at 06:56
0

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.

Community
  • 1
  • 1
  • I'm sorry but I don't really like your solutions. I was thinking too much in these days and probably I've found out something better. We can use a pre-filled CoreData database (using some editor like CoreData Editor / manager (?)) and copy the entire DB into the .nosyc directory because we need all the records must have same UUID. The contents of .nosync dir is not synched in the cloud, but the transaction logs for new actions on that records will be synched. I think this is the best approach. The final problem is: what if Apple changes the CoreData pre-(generated and copied)-filled database? – Progeny May 15 '12 at 18:29
  • My solution is not any perfect indeed. It's just what I managed to work out with a limited time. Did your solution completely work out for your task? My pre-filled data is "categories" and I need "items" added to them on different devices to belong to the same category. Will that happen according to your experience? I didn't quite get your last question. Are you afraid of Apple changing core data bd format? – Kostiantyn Sokolinskyi May 16 '12 at 07:31
  • I was developing this case 3 months ago so I don't remember every detail right now. But I think I tried your approach as well and I didn't get data consistency - "categories" on different devices were indeed different entities from iCLoud perspective. I would be grateful if you report your results. – Kostiantyn Sokolinskyi May 16 '12 at 07:35
  • Yeah, I'm afraid Apple might change something the core data db file. I've read somewhere on the web that Apple had changed from iOS 4 to iOS 5 the way it stores integers in CoreData. – Progeny May 16 '12 at 11:28
  • hm. no idea actually. But apple can easily change it, indeed. So did your method completely work out for you? – Kostiantyn Sokolinskyi May 17 '12 at 07:32
0

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/

adib
  • 8,285
  • 6
  • 52
  • 91