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.