0

I have an app that uses a single UIManagedDocument as the data store which I access through a singleton using the method in Justin Driscoll's blog.

This works fine, but when I set my ubiquity keys for the persistentStoreOptions and change state notifications, the app only runs in the sandbox and there is no network activity.

Here is the execution flow ... 1. App opens to a landing screen where the singleton is created. 2. Once the UIManagedDocument is open I check for cloud permissions and if all is good to go and we have a ubiquity container, I overwrite the persistentStoreOptions with the cloud contentNameKey and contentUrlKey options.

Here is the code …

//  DataStore.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>

typedef void (^OnDocumentReady) (UIManagedDocument *appDataStore);

@interface DataStore : NSObject

@property (strong, nonatomic) UIManagedDocument *appDataStore;

+ (DataStore *)sharedDocumentHandler;
- (void)performWithDocument:(OnDocumentReady)onDocumentReady;

@end


//  DataStore.m
#import "DataStore.h"
#import "HashDefines.h"

@interface DataStore ()

@property (nonatomic)BOOL preparingDocument;
@property (nonatomic, strong) NSFileCoordinator *coordinator;

@property (strong, nonatomic) NSString *localDocumentsPath;
@property (strong, nonatomic) NSURL *localDocumentsURL;
@property (strong, nonatomic) NSURL *localFileURL;

@end;

@implementation DataStore

@synthesize appDataStore = _appDataStore;
@synthesize localDocumentsPath = _localDocumentsPath;
@synthesize localDocumentsURL = _localDocumentsURL;
@synthesize localFileURL = _localFileURL;

static DataStore *_sharedInstance;
@synthesize coordinator = _coordinator;

#define LOCAL_DATA_STORE_FILE_NAME @“QR App DataStore"

#pragma mark - synthesiser overiders

+ (DataStore *)sharedDocumentHandler
{
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        _sharedInstance = [[self alloc] init];
    });

    return _sharedInstance;
}

#pragma mark - Key Paths

#pragma mark Local Documents
- (NSString *) localDocumentsPath
{
    if (!_localDocumentsPath) {
        _localDocumentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    }

    return _localDocumentsPath;
}

- (NSURL *) localDocumentsURL
{
    if (!_localDocumentsURL) {
        _localDocumentsURL = [NSURL fileURLWithPath:self.localDocumentsPath];
    }

    return _localDocumentsURL;
}

#pragma mark - File URLs

- (NSURL *) localFileURL: (NSString *) filename
{
    if (!filename) return nil;
    NSURL *fileURL = [[self localDocumentsURL] URLByAppendingPathComponent:filename];
    return fileURL;
}


#pragma mark - the juicy bits


#pragma mark - initialisers

- (id)init
{
    self = [super init];
    if (self) {

        NSLog(@"appDataStore does NOT exist ... building one now");

        [self initCoreDataInTheSandbox];

    }
    return self;
}

- (void)initCoreDataInTheSandbox
{
    NSURL *localURL = [self localFileURL:LOCAL_DATA_STORE_FILE_NAME];
    self.appDataStore = [[UIManagedDocument alloc] initWithFileURL:localURL];

    // Set our document up for automatic migrations
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    self.appDataStore.persistentStoreOptions = options;

    NSLog(@"1. DS persistentStoreOptions: %@", self.appDataStore.persistentStoreOptions);
}

- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
    NSLog(@"1. DS Begin performWithDocument: %@", self.appDataStore);

    void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success) {
        onDocumentReady(self.appDataStore);
        NSLog(@"2. DS into the block ... onDocumentReady:");
        self.preparingDocument = NO; // release in completion handler
    };

    if(!self.preparingDocument) {
        NSLog(@"3. DS preparing document: dataStore.appDataStore: %@", self.appDataStore);

        // "lock", so no one else enter here
        self.preparingDocument = YES;

        if(![[NSFileManager defaultManager] fileExistsAtPath:[self.appDataStore.fileURL path]]) {
            NSLog(@"4. DS creating document: dataStore.appDataStore: %@", self.appDataStore);
            [self.appDataStore saveToURL:self.appDataStore.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
            [self.appDataStore openWithCompletionHandler:OnDocumentDidLoad];
        } else if (self.appDataStore.documentState == UIDocumentStateClosed) {
            NSLog(@"5. DS dataStore.appDataStore.documentState: %d ... closed", self.appDataStore.documentState);
            [self.appDataStore openWithCompletionHandler:OnDocumentDidLoad];
        } else if (self.appDataStore.documentState == UIDocumentStateSavingError) {
            NSLog(@"6. DS dataStore.appDataStore.documentState: %d ... saving error", self.appDataStore.documentState);
            [self.appDataStore openWithCompletionHandler:OnDocumentDidLoad];
        } else if (self.appDataStore.documentState == UIDocumentStateNormal) {
            NSLog(@"7. DS dataStore.appDataStore.documentState: %d ... open", self.appDataStore.documentState);
            OnDocumentDidLoad(YES);
        }
    } else {
        // try until document is ready (opened or created by some other call)
        NSLog(@"8. DS preparing document - NOT ... trying again");
        [self performSelector:@selector(performWithDocument:) withObject:onDocumentReady afterDelay:0.5];
    }

    NSLog(@"9. DS Exiting performWithDocument: %@", self.appDataStore);
}

@end


// CloudConnector.h
#import "DataStore.h"

@interface CloudConnector : NSObject

- (void)hookUpCloudForDocument:(UIManagedDocument *)document;

(NSManagedObjectContext*)managedObjectContext;
- (NSString *) documentState: (int) state;

@end

#define ICLOUD_TOKEN_KEY @“com.apple.QR-App.UbiquityIdentityToken"
#define CLOUD_IS_UBIQUITOUS @"sweet ubiquity"
#define LOCAL_DATA_STORE_FILE_NAME @“QR App DataStore"
#define CLOUD_DATA_STORE_FILE_NAME @"com~app~QR-App~cloudstore"


//CloudConnector.m
#import "CloudConnector.h"
#import "HashDefines.h"

@interface CloudConnector ()

@property (strong, nonatomic) NSString *localDocumentsPath;
@property (strong, nonatomic) NSURL *localDocumentsURL;
@property (strong, nonatomic) NSURL *localFileURL;
@property (strong, nonatomic) NSURL *ubiquityDataFileURL;

@property (nonatomic)BOOL preparingDocument;
@property (nonatomic, strong) NSFileCoordinator *coordinator;

- (NSURL *) localFileURL: (NSString *) filename;
- (NSURL *) ubiquityDataFileURL: (NSString *) filename;
- (BOOL) isLocal: (NSString *) filename;
- (NSString *) documentState: (int) state;

@end;

@implementation CloudConnector

@synthesize coordinator = _coordinator;
@synthesize localDocumentsPath = _localDocumentsPath;
@synthesize localDocumentsURL = _localDocumentsURL;
@synthesize localFileURL = _localFileURL;
@synthesize  ubiquityDataFileURL = _ubiquityDataFileURL;

#pragma mark - synthesiser overiders




#pragma mark - the juicy bits

- (void)checkUbiquityStatus
{
    id currentiCloudToken = [[NSFileManager defaultManager] ubiquityIdentityToken];

    if (currentiCloudToken) {
        NSData *newTokenData =
        [NSKeyedArchiver archivedDataWithRootObject: currentiCloudToken];
        [[NSUserDefaults standardUserDefaults]
         setObject: newTokenData
         forKey: ICLOUD_TOKEN_KEY];
    } else {
        [[NSUserDefaults standardUserDefaults]
         removeObjectForKey: ICLOUD_TOKEN_KEY];
    }
}

#pragma mark - Key Paths

#pragma mark Local Documents
- (NSString *) localDocumentsPath
{
    if (!_localDocumentsPath) {
        _localDocumentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    }

    return _localDocumentsPath;
}

- (NSURL *) localDocumentsURL
{
    if (!_localDocumentsURL) {
        _localDocumentsURL = [NSURL fileURLWithPath:self.localDocumentsPath];
    }

    return _localDocumentsURL;
}

#pragma mark - File URLs
- (NSURL *) localFileURL: (NSString *) filename
{
    if (!filename) return nil;
    NSURL *fileURL = [[self localDocumentsURL] URLByAppendingPathComponent:filename];
    return fileURL;
}

- (NSURL *) ubiquityDataFileURL: (NSString *) filename forContainer: (NSString *) container
{
    if (!filename) return nil;
    NSURL *fileURL = [[self ubiquityDataURLForContainer:container] URLByAppendingPathComponent:filename];
    return fileURL;
}


- (NSURL *) ubiquityDataFileURL: (NSString *) filename
{
    return [self ubiquityDataFileURL:filename forContainer:nil];
}


#pragma mark Ubiquity Data
- (NSURL *) ubiquityDataURLForContainer: (NSString *) container
{
    return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:container];
}

- (NSArray *) contentsOfUbiquityDataFolderForContainer: (NSString *) container
{
    NSURL *targetURL = [self ubiquityDataURLForContainer:container];
    if (!targetURL) return nil;

    NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:targetURL.path error:nil];
    return array;
}

- (BOOL) isLocal: (NSString *) filename
{
    if (!filename) return NO;
    NSURL *targetURL = [self localFileURL:filename];
    if (!targetURL) return NO;
    return [[NSFileManager defaultManager] fileExistsAtPath:targetURL.path];
}


- (NSString *) documentState: (int) state
{
    if (!state) return @"Document state is normal";

    NSMutableString *string = [NSMutableString string];
    if ((state & UIDocumentStateClosed) != 0)
        [string appendString:@"Document is closed\n"];
    if ((state & UIDocumentStateInConflict) != 0)
        [string appendString:@"Document is in conflict"];
    if ((state & UIDocumentStateSavingError) != 0)
        [string appendString:@"Document is experiencing saving error"];
    if ((state & UIDocumentStateEditingDisabled) != 0)
        [string appendString:@"Document editing is disbled" ];

    return string;
}


#pragma mark - initialisers


- (void)hookUpCloudForDocument:(UIManagedDocument *)document
{
    // checking for ubiquity
    NSLog(@"checking for ubiquity");

    [self checkUbiquityStatus];

    // THE FOLLOWING CODE DOESN'T WORK
    if ([[NSUserDefaults standardUserDefaults] objectForKey:ICLOUD_TOKEN_KEY]) {
        // cloud permissions are good to go.
        NSLog(@"cloud permissions are good to go.");

        dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
            NSURL *url = [[NSFileManager defaultManager]
                          URLForUbiquityContainerIdentifier: nil];

            NSLog(@"bikky url: %@", url);

            if (url) { // != nil) {
                // Your app can write to the ubiquity container

                dispatch_async (dispatch_get_main_queue (), ^(void) {
                    // On the main thread, update UI and state as appropriate

                    [self setupCoreDataInTheCloudForDocument:document];
                });
            }
        });
    }
}

- (void) setupCoreDataInTheCloudForDocument:(UIManagedDocument *)document
{
    NSLog(@"1. cc.setupCoreDataInTheCloudForDocument: %@", document);

    NSURL *localURL = [self localFileURL:LOCAL_DATA_STORE_FILE_NAME];
    NSURL *cloudURL = [self ubiquityDataFileURL:CLOUD_DATA_STORE_FILE_NAME];

    // Set the persistent store options to point to the cloud
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
//                             [document.fileURL lastPathComponent], NSPersistentStoreUbiquitousContentNameKey,
                             localURL, NSPersistentStoreUbiquitousContentNameKey,
                             cloudURL, NSPersistentStoreUbiquitousContentURLKey,
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                             nil];

    document.persistentStoreOptions = options;
    NSLog(@"2. cc.document.persistentStoreOptions: %@", document.persistentStoreOptions);

    // Register as presenter
    self.coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:document];
    [NSFileCoordinator addFilePresenter:document];


//    // THIS CODE IS INLINE FROM iOS 5 COOK BOOK
//    // I EXECUTE THIS IN (void)performWithDocument:(OnDocumentReady)onDocumentReady
//
//    // Check at the local sandbox
//    if ([self isLocal:LOCAL_DATA_STORE_FILE_NAME])
//    {
//        NSLog(@"Attempting to open existing file");
//        [document openWithCompletionHandler:^(BOOL success){
//            if (!success) {NSLog(@"Error opening file"); return;}
//            NSLog(@"File opened");
//        }];
//    }
//    else
//    {
//        NSLog(@"Creating file.");
//        // 1. save it out, 2. close it, 3. read it back in.
//        [document saveToURL:localURL
//                    forSaveOperation:UIDocumentSaveForCreating
//                   completionHandler:^(BOOL success){
//                       if (!success) { NSLog(@"7. Error creating file"); return; }
//                       NSLog(@"File created");
//                       [document closeWithCompletionHandler:^(BOOL success){
//                           NSLog(@"Closed new file: %@", success ? @"Success" : @"Failure");
//
//                           [document openWithCompletionHandler:^(BOOL success){
//                               if (!success) {NSLog(@"Error opening file for reading."); return;}
//                               NSLog(@"File opened for reading.");
//                           }];
//                       }];
//                   }];
//    }
//}

@end

I call the singleton as follows …

- (void)viewDidLoad
{
    [super viewDidLoad];

    if (!self.codeGeneratorDataStore) {

        NSLog(@"MVC.viewDidLoad: Grab local instance of document from data store singleton");

        [[DataStore sharedDocumentHandler] performWithDocument:^(UIManagedDocument *document) {
            self.codeGeneratorDataStore = document;
            // Do stuff with the document, set up a fetched results controller, whatever.
//            NSLog(@"IN:  _codeGeneratorDataStore.documentState : %u", _codeGeneratorDataStore.documentState);

            if (![self.codeGeneratorDataStore.persistentStoreOptions objectForKey:NSPersistentStoreUbiquitousContentNameKey]) {
                NSLog(@"MVC.viewDidLoad: We have a document, hooking up cloud now ... \n%@", document);
                [self.cloudConnector hookUpCloudForDocument:self.codeGeneratorDataStore];
            }

            [self setupFetchedResultsController];
        }];

        NSLog(@"GVC.viewDidLoad: Subscribing to notifications");
        [self subscribeToCloudNotifications];

    }

    [self checkDocumentStatus];    
}

Then I check the document status …

- (void)checkDocumentStatus {

    NSLog(@"MVC.checkDocumentStatus\n Document: %@\n persistentStoreOptions: %@", self.codeGeneratorDataStore, self.codeGeneratorDataStore.persistentStoreOptions);
    if (![[
           NSFileManager defaultManager] fileExistsAtPath:[self.codeGeneratorDataStore.fileURL path]]) {
        [self.codeGeneratorDataStore saveToURL:self.codeGeneratorDataStore.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            NSLog(@"1. MVC self.codeGeneratorDataStore.documentState: %@", [NSString stringWithFormat:@"%u", self.codeGeneratorDataStore.documentState]);
        }];
    } else if (self.codeGeneratorDataStore.documentState == UIDocumentStateClosed) {
        [self.codeGeneratorDataStore openWithCompletionHandler:^(BOOL succes) {
            NSLog(@"2. MVC self.codeGeneratorDataStore.documentState: %@", [NSString stringWithFormat:@"%u", self.codeGeneratorDataStore.documentState]);               
        }];
    } else if (self.codeGeneratorDataStore.documentState == UIDocumentStateNormal) {
        NSLog(@"3. MVC self.codeGeneratorDataStore.documentState: %@", [NSString stringWithFormat:@"%u", self.codeGeneratorDataStore.documentState]);
    }
}

Setup and register for notifications ...

- (void)subscribeToCloudNotifications
{
    // subscribe to the NSPersistentStoreDidImportUbiquitousContentChangesNotification notification
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(documentStateChanged:)
                                                 name:UIDocumentStateChangedNotification
                                               object:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(documentContentsDidUpdate:)
                                                 name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
                                               object:nil];

    NSLog(@"MVC.subscribeToCloudNotifications: notification subscriptions are good to go");
}

- (void) documentContentsDidUpdate: (NSNotification *) notification
{
    NSLog(@"CMVC notification: documentContentsDidUpdate");

    NSDictionary *userInfo = notification.userInfo;
    [self.codeGeneratorDataStore.managedObjectContext performBlock:^{[self mergeiCloudChanges:userInfo forContext:self.codeGeneratorDataStore.managedObjectContext];}];
}

// When notified about a cloud update, start merging changes
- (void)documentStateChanged: (NSNotification *)notification
{
    NSLog(@"CMVC notification: documentStateChanged");
//    NSLog(@"Document state change: %@", [CloudHelper documentState:self.codeGeneratorDataStore.documentState]);

    UIDocumentState documentState = self.codeGeneratorDataStore.documentState;
    if (documentState & UIDocumentStateInConflict)
    {
        // This application uses a basic newest version wins conflict resolution strategy
        NSURL *documentURL = self.codeGeneratorDataStore.fileURL;
        NSArray *conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:documentURL];
        for (NSFileVersion *fileVersion in conflictVersions) {
            fileVersion.resolved = YES;
        }
        [NSFileVersion removeOtherVersionsOfItemAtURL:documentURL error:nil];
    }
}

// Merge the iCloud changes into the managed context
- (void)mergeiCloudChanges:(NSDictionary*)userInfo forContext:(NSManagedObjectContext*)managedObjectContext
{
    @autoreleasepool
    {
        //        NSLog(@"Merging changes from cloud");

        NSMutableDictionary *localUserInfo = [NSMutableDictionary dictionary];

        NSSet *allInvalidations = [userInfo objectForKey:NSInvalidatedAllObjectsKey];
        NSString *materializeKeys[] = { NSDeletedObjectsKey, NSInsertedObjectsKey };

        if (nil == allInvalidations)
        {

            // (1) we always materialize deletions to ensure delete propagation happens correctly, especially with
            // more complex scenarios like merge conflicts and undo.  Without this, future echoes may
            // erroreously resurrect objects and cause dangling foreign keys
            // (2) we always materialize insertions to make new entries visible to the UI

            int c = (sizeof(materializeKeys) / sizeof(NSString *));
            for (int i = 0; i < c; i++)
            {
                NSSet *set = [userInfo objectForKey:materializeKeys[i]];
                if ([set count] > 0)
                {
                    NSMutableSet *objectSet = [NSMutableSet set];
                    for (NSManagedObjectID *moid in set)
                        [objectSet addObject:[managedObjectContext objectWithID:moid]];

                    [localUserInfo setObject:objectSet forKey:materializeKeys[i]];
                }
            }

            // (3) we do not materialize updates to objects we are not currently using
            // (4) we do not materialize refreshes to objects we are not currently using
            // (5) we do not materialize invalidations to objects we are not currently using

            NSString *noMaterializeKeys[] = { NSUpdatedObjectsKey, NSRefreshedObjectsKey, NSInvalidatedObjectsKey };
            c = (sizeof(noMaterializeKeys) / sizeof(NSString*));
            for (int i = 0; i < 2; i++)
            {
                NSSet *set = [userInfo objectForKey:noMaterializeKeys[i]];
                if ([set count] > 0)
                {
                    NSMutableSet *objectSet = [NSMutableSet set];
                    for (NSManagedObjectID *moid in set)
                    {
                        NSManagedObject *realObj = [managedObjectContext objectRegisteredForID:moid];
                        if (realObj)
                            [objectSet addObject:realObj];
                    }

                    [localUserInfo setObject:objectSet forKey:noMaterializeKeys[i]];
                }
            }

            NSNotification *fakeSave = [NSNotification notificationWithName:NSManagedObjectContextDidSaveNotification object:self userInfo:localUserInfo];
            [managedObjectContext mergeChangesFromContextDidSaveNotification:fakeSave];

        }
        else
            [localUserInfo setObject:allInvalidations forKey:NSInvalidatedAllObjectsKey];

        [managedObjectContext processPendingChanges];

        //        [self performSelectorOnMainThread:@selector(performFetch) withObject:nil waitUntilDone:NO];
    }
}

and we’re good to go - the app runs but with no network activity. Why aren't the transaction logs being uploaded?

Any assistance would be greatly appreciated here. Its been doing my head in for a week now and i’m at my wit’s end.

user1809511
  • 51
  • 1
  • 3
  • Can't really see what you are doing but you have to migrate a store to iCloud if it already exists. You might want to take a look at the various examples here covering both UIManagedDocument and the native Core Data stack. http://ossh.com.au/design-and-technology/software-development/ – Duncan Groenewald Jan 31 '14 at 03:30
  • Also be aware that UIManagedDocument is opened asynchronously so you have to use a callback - it's not clear whether you are doing that. It looks a bit like you have all your initialisation happening in viewDidLoad so that's likely to result in the UI trying to display stuff before the document has been opened. UIManagedDocument does not play nicely with iCloud without some work - see the link above. – Duncan Groenewald Jan 31 '14 at 03:35

1 Answers1

0

I answered this question previously in email, but I'm positing my answer here as well for completeness.

Is this an iOS 7-only app, or does it need to support iOS 5 or 6 as well?

I would not recommend mixing iCloud and Core Data in anything that still has to support iOS 5 and 6. However, the new support for iOS 7 seems pretty solid. I haven’t deployed a multi-million user app with it—so I wouldn’t say it’s battle tested, but it seems to be a lot more robust. In my preliminary testing, it handled everything I threw at it with no problems.

Also, is the sample code that you’re following up-to-date for iOS 7? It looks like it’s following some older patterns. This could cause a lot of problems, since the way the system operates has changed considerably in iOS 7.

For example, in iOS 7, we generally only need to set the NSPersistentStoreUbiquitousContentNameKey. Unless we’re trying to support a legacy core data stack, we don’t need to set the content URL key.

Also, there is a huge difference in how the core data stack is set up under iOS 7. When we first set up an iCloud persistent store, iOS 7 will create a local copy (the fallback store) for us automatically. We can use the store—but the changes will only be stored locally. When the iCloud version is ready, it will then send a NSPersistentStoreCoordinatorStoresWillChangeNotification. We’re supposed to save any changes and then reset our managed object context.

Again, it looks like your code is manually setting up its own fallback store—which may be part of the problem.

If you’re working from old examples, I’d actually recommend throwing that code away and starting over. Watch the “What’s New in Core Data and iCloud” video from WWDC 2013. It does a great rundown on the new technologies, and the current best practices.

My guess is that you’re getting the fallback store, but it’s never switching over to the iCloud store. Look in the console log when you run the app. You should see something like the following:

-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](754): CoreData: p Ubiquity:
 mobile~C9C8554A-BD44-43C3-AC54-603046EF0162:com~freelancemad p science~HealthBeat

Using local storage: 1

Here, “Using local storage: 1” means you’re using the fallback store. In a minute or two (or sometimes longer) you’ll see a similar message with “Using local storage: 0”. That means you’re now using the iCloud store.

Also, when you run the application, be sure to open the Debug Navigator in Xcode. It should have a profiler for iCloud, that shows you whenever data was uploaded to or downloaded from the cloud.

I hope that helps,

-Rich-