From 3a130eaa47f0980f35026fa3296e26c71fc0c379 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 27 May 2012 14:21:02 +0200 Subject: [PATCH 01/35] Don't wait for -URLForUbiquityContainerIdentifier: when iCloud isn't even enabled. --- iCloudStoreManager/UbiquityStoreManager.m | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 42c2508..17e268a 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -481,10 +481,12 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple // Clear previous persistentStore [self clearPersistentStore]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; - - BOOL usingiCloud = ([coreDataCloudContent length] != 0) && willUseiCloud; + NSString* coreDataCloudContent = nil; + if (willUseiCloud) { + NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; + coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; + } + BOOL usingiCloud = ([coreDataCloudContent length] != 0); if (usingiCloud) { // iCloud is available From 49247f9326fb33a523adb5f49192316889280004 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 14 Jul 2012 23:56:43 +0200 Subject: [PATCH 02/35] Warning fixes with regards to braces. --- iCloudStoreManager/UbiquityStoreManager.m | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 8553d69..ccde077 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -102,11 +102,12 @@ - (void)deleteStoreDirectory { NSError *error = nil; [fileManager removeItemAtPath:databaseContent error:&error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:databaseContent]; else NSLog(@"Error deleting old store: %@", error); + } } } @@ -119,11 +120,12 @@ - (void)createStoreDirectoryIfNecessary { NSError *error = nil; [fileManager createDirectoryAtPath:databaseContent withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:databaseContent]; else NSLog(@"Error creating database directory: %@", error); + } } } @@ -140,11 +142,12 @@ - (void)deleteTransactionLogs { NSString *path = [[self transactionLogsURL] path]; NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager removeItemAtPath:path error:&error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:path]; else NSLog(@"Error deleting local store: %@", error); + } } @@ -162,11 +165,12 @@ - (void)deleteTransactionLogsForUUID:(NSString *)uuid { NSError *error = nil; NSString *transactionLogsForUUID = [[self transactionLogsURLForUUID:uuid] path]; [fileManager removeItemAtPath:transactionLogsForUUID error:&error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:transactionLogsForUUID]; else NSLog(@"Error deleting local store: %@", error); + } } } } @@ -323,11 +327,12 @@ - (void)hardResetLocalStorage { if (_hardResetEnabled) { NSError *error; [[NSFileManager defaultManager] removeItemAtURL:localStoreURL__ error:&error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:localStoreURL__]; else NSLog(@"Error deleting local store: %@", error); + } } } @@ -367,11 +372,12 @@ - (void)clearPersistentStore { [persistentStoreCoordinator__ removePersistentStore:persistentStore__ error:&error]; persistentStore__ = nil; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseClearStore context:persistentStore__]; else NSLog(@"Error removing persistent store: %@", error); + } } } @@ -422,11 +428,12 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt options: additionalStoreOptions__ error: &error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; else NSLog(@"Prepping migrated store error: %@", error); + } error = nil; persistentStore__ = [psc migratePersistentStore: migratedStore @@ -449,13 +456,14 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt } [psc unlock]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL]; else { NSLog(@"Persistent store error: %@", error); NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); } + } } - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid completionBlock:(void (^)(BOOL usingiCloud))completionBlock { @@ -508,11 +516,12 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple URL: localStoreURL__ options: options error: &error]; - if (error) + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; else NSLog(@"Persistent store error: %@", error); + } [psc unlock]; } From 1614e65222a543a75cdf4e5f757f06e39b5979aa Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 29 Jul 2012 14:11:15 +0200 Subject: [PATCH 03/35] Make all NSFileManager removal operations coordinated. --- iCloudStoreManager/UbiquityStoreManager.m | 207 +++++++++++++--------- 1 file changed, 123 insertions(+), 84 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index ccde077..f33ca30 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -38,6 +38,7 @@ @interface UbiquityStoreManager () { @property (nonatomic) NSString *localUUID; @property (nonatomic) NSString *iCloudUUID; +@property (nonatomic, readwrite) BOOL iCloudEnabled; - (NSString *)freshUUID; - (void)registerForNotifications; @@ -62,14 +63,14 @@ - (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NS model__ = model; localStoreURL__ = storeURL; persistentStorageQueue = dispatch_queue_create([@"PersistentStorageQueue" UTF8String], DISPATCH_QUEUE_SERIAL); - + // Start iCloud connection [self updateLocalCopyOfiCloudUUID]; - + [self checkiCloudStatus]; [self registerForNotifications]; } - + return self; } @@ -85,7 +86,7 @@ - (NSURL *)iCloudStoreURLForUUID:(NSString *)uuid { NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; NSString *storePath = [databaseContent stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", uuid]]; - + return [NSURL fileURLWithPath:storePath]; } @@ -97,29 +98,42 @@ - (void)deleteStoreDirectory { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - + if ([fileManager fileExistsAtPath:databaseContent]) { NSError *error = nil; - [fileManager removeItemAtPath:databaseContent error:&error]; - - if (error) { + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] + coordinateWritingItemAtURL:[NSURL fileURLWithPath:databaseContent] options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![fileManager removeItemAtPath:databaseContent error:&error_]) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) + [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore + context:databaseContent]; + else + NSLog(@"Error deleting iCloud store: %@", error_); + } + }]; + + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:databaseContent]; + [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore + context:databaseContent]; else - NSLog(@"Error deleting old store: %@", error); + NSLog(@"Error coordinating for deletion of iCloud store: %@", error); } - } + } } - (void)createStoreDirectoryIfNecessary { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - + if (![fileManager fileExistsAtPath:databaseContent]) { NSError *error = nil; [fileManager createDirectoryAtPath:databaseContent withIntermediateDirectories:YES attributes:nil error:&error]; - + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:databaseContent]; @@ -133,22 +147,33 @@ - (NSURL *)transactionLogsURL { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; - + return [NSURL fileURLWithPath:coreDataCloudContent]; } - (void)deleteTransactionLogs { NSError *error = nil; - NSString *path = [[self transactionLogsURL] path]; + NSURL *transactionLogsURL = [self transactionLogsURL]; NSFileManager *fileManager = [NSFileManager defaultManager]; - [fileManager removeItemAtPath:path error:&error]; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] + coordinateWritingItemAtURL:transactionLogsURL options:NSFileCoordinatorWritingForDeleting error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![fileManager removeItemAtURL:transactionLogsURL error:&error_]) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) + [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteLogs + context:transactionLogsURL]; + else + NSLog(@"Error deleting transaction logs: %@", error_); + } + }]; if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:path]; + [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs + context:transactionLogsURL]; else - NSLog(@"Error deleting local store: %@", error); + NSLog(@"Error coordinating for deletion of transaction logs: %@", error); } - } - (NSURL *)transactionLogsURLForUUID:(NSString *)uuid { @@ -159,19 +184,33 @@ - (void)deleteTransactionLogsForUUID:(NSString *)uuid { if (uuid) { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - + // Can only continue if iCloud is available if (cloudURL) { NSError *error = nil; - NSString *transactionLogsForUUID = [[self transactionLogsURLForUUID:uuid] path]; - [fileManager removeItemAtPath:transactionLogsForUUID error:&error]; + NSURL *transactionLogsURLForUUID = [self transactionLogsURLForUUID:uuid]; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] + coordinateWritingItemAtURL:transactionLogsURLForUUID options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![fileManager removeItemAtURL:transactionLogsURLForUUID error:&error_]) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) + [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteLogs + context:transactionLogsURLForUUID]; + else + NSLog(@"Error deleting transaction logs for UUID: %@: %@", uuid, error_); + } + }]; + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs context:transactionLogsForUUID]; + [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs + context:transactionLogsURLForUUID]; else - NSLog(@"Error deleting local store: %@", error); + NSLog(@"Error coordinating for deletion of transaction logs for UUID: %@: %@", uuid, error); } - } + } } } @@ -224,7 +263,7 @@ - (void)alertView:(OS_Alert *)alertView didDismissWithButtonIndex:(NSInteger)but [self didSwitchToiCloud:NO]; } } - + if (alertView == switchToiCloudAlert) { if (buttonIndex == 1) { // Switch to using data from iCloud @@ -234,7 +273,7 @@ - (void)alertView:(OS_Alert *)alertView didDismissWithButtonIndex:(NSInteger)but [self didSwitchToiCloud:NO]; } } - + if (alertView == switchToLocalAlert) { if (buttonIndex == 1) { // Switch to using data from iCloud @@ -253,7 +292,7 @@ - (void)moveDataToiCloudAlert { delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: @"Move Data", nil]; - [moveDataAlert show]; + [moveDataAlert show]; #else moveDataAlert = [NSAlert alertWithMessageText:[self moveDataToiCloudTitle] defaultButton:@"Move Data" @@ -309,7 +348,7 @@ - (void)switchToLocalDataAlert { delegate: self cancelButtonTitle: @"Cancel" otherButtonTitles: @"Continue", nil]; - [switchToLocalAlert show]; + [switchToLocalAlert show]; #else switchToLocalAlert = [NSAlert alertWithMessageText:[self switchToLocalDataTitle] defaultButton:@"Continue" @@ -341,11 +380,11 @@ - (void)hardResetCloudStorage { [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { [self deleteStoreDirectory]; [self deleteTransactionLogs]; - + // Setting iCloudUUID to nil will propagate to all other devices, // and automatically force them to switch over to their local stores - - self.iCloudUUID = nil; + + self.iCloudUUID = nil; self.localUUID = nil; self.iCloudEnabled = NO; }]; @@ -357,10 +396,10 @@ - (NSArray *)fileList { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - + if (cloudURL) fileList = [fileManager subpathsAtPath:[cloudURL path]]; - + return fileList; } @@ -371,7 +410,7 @@ - (void)clearPersistentStore { NSError *error = nil; [persistentStoreCoordinator__ removePersistentStore:persistentStore__ error:&error]; persistentStore__ = nil; - + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseClearStore context:persistentStore__]; @@ -382,10 +421,10 @@ - (void)clearPersistentStore { } - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - + if (persistentStoreCoordinator__ == nil) { persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - + NSString *uuid = (self.iCloudEnabled) ? self.localUUID : nil; [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:nil]; } @@ -398,10 +437,10 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt NSAssert([[psc persistentStores] count] == 0, @"There were more persistent stores than expected"); [self createStoreDirectoryIfNecessary]; - + NSError *error = nil; NSURL *transactionLogsURL = [self transactionLogsURLForUUID:uuid]; - + options = [NSMutableDictionary dictionaryWithObjectsAndKeys: uuid, NSPersistentStoreUbiquitousContentNameKey, transactionLogsURL, NSPersistentStoreUbiquitousContentURLKey, @@ -409,21 +448,21 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; [options addEntriesFromDictionary:additionalStoreOptions__]; - + [psc lock]; - + NSURL *cloudStoreURL = [self iCloudStoreURLForUUID:uuid]; if (migrate) { // Clear old registered notifcations. This was required to address an exception that occurs when using // a persistent store on iCloud setup by another device (Object's persistent store is not reachable // from this NSManagedObjectContext's coordinator) - [[NSNotificationCenter defaultCenter] removeObserver: self + [[NSNotificationCenter defaultCenter] removeObserver: self name: NSPersistentStoreDidImportUbiquitousContentChangesNotification object: psc]; // Add the store to migrate NSPersistentStore * migratedStore = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil + configuration: nil URL: localStoreURL__ options: additionalStoreOptions__ error: &error]; @@ -436,15 +475,15 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt } error = nil; - persistentStore__ = [psc migratePersistentStore: migratedStore + persistentStore__ = [psc migratePersistentStore: migratedStore toURL: cloudStoreURL options: options withType: NSSQLiteStoreType error: &error]; - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification + [[NSNotificationCenter defaultCenter]addObserver: self + selector: @selector(mergeChanges:) + name: NSPersistentStoreDidImportUbiquitousContentChangesNotification object: psc]; } else { @@ -455,7 +494,7 @@ - (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentSt error: &error]; } [psc unlock]; - + if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL]; @@ -473,29 +512,29 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple else NSLog(@"Setting up store with UUID: %@", uuid); BOOL willUseiCloud = (uuid != nil); - + // TODO: Check for use case where user checks out of one iCloud account, and logs into another! - + // TODO: Test deletion from Settings App -> Manage Data -> Delete Data (nuke option) - + NSPersistentStoreCoordinator* psc = persistentStoreCoordinator__; - + // Do this asynchronously since if this is the first time this particular device is syncing with preexisting // iCloud content it may take a long long time to download dispatch_async(persistentStorageQueue, ^{ NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableDictionary *options; - + // Clear previous persistentStore [self clearPersistentStore]; - + NSString* coreDataCloudContent = nil; if (willUseiCloud) { NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; } BOOL usingiCloud = ([coreDataCloudContent length] != 0); - + if (usingiCloud) { // iCloud is available [self migrateToiCloud:migrate persistentStoreCoordinator:psc with:uuid]; @@ -507,14 +546,14 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; [options addEntriesFromDictionary:additionalStoreOptions__]; - + [psc lock]; - + NSError *error = nil; persistentStore__ = [psc addPersistentStoreWithType: NSSQLiteStoreType configuration: nil - URL: localStoreURL__ - options: options + URL: localStoreURL__ + options: options error: &error]; if (error) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) @@ -522,24 +561,24 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple else NSLog(@"Persistent store error: %@", error); } - + [psc unlock]; } if (![[psc persistentStores] count]) return; - + _isReady = YES; - + NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); - + NSString *usingiCloudString = (usingiCloud) ? @" using iCloud!" : @"!"; - + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Asynchronously added persistent store%@", usingiCloudString]]; else NSLog(@"Asynchronously added persistent store%@", usingiCloudString); - + if (completionBlock) { completionBlock(usingiCloud); } @@ -554,7 +593,7 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple - (void)useCloudStorage { if (persistentStoreCoordinator__ == nil) persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - + [self migrate:NO andUseCloudStorageWithUUID:self.iCloudUUID completionBlock:^(BOOL usingiCloud) { if (usingiCloud) { self.localUUID = self.iCloudUUID; @@ -566,7 +605,7 @@ - (void)useCloudStorage { - (void)useLocalStorage { if (persistentStoreCoordinator__ == nil) persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - + [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { self.localUUID = nil; self.iCloudEnabled = NO; @@ -592,7 +631,7 @@ - (void)setupCloudStorageWithUUID:(NSString *)uuid { - (void)replaceiCloudStoreWithUUID:(NSString *)uuid { if (persistentStoreCoordinator__ == nil) persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - + [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:^(BOOL usingiCloud) { if (usingiCloud) { self.localUUID = uuid; @@ -604,7 +643,7 @@ - (void)replaceiCloudStoreWithUUID:(NSString *)uuid { [self deleteStoreDirectory]; [self deleteTransactionLogs]; } - + self.localUUID = nil; self.iCloudEnabled = NO; } @@ -634,14 +673,14 @@ - (void)checkiCloudStatus { - (void)useiCloudStore:(BOOL)willUseiCloud alertUser:(BOOL)alertUser { // To provide the option of using iCloud immediately upon first running of an app, // make sure a persistentStoreCoordinator exists. - + if (persistentStoreCoordinator__ == nil) persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - + if (willUseiCloud) { if (!self.iCloudEnabled) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - + // If an iCloud store already exists, ask the user if they want to switch over to iCloud if (cloud) { if (self.iCloudUUID) { @@ -693,9 +732,9 @@ - (void)setICloudEnabled:(BOOL)enabled { - (NSString *)freshUUID { CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); - + CFRelease(uuidRef); - + return (__bridge_transfer NSString *)uuidStringRef; } @@ -740,11 +779,11 @@ - (void)keyValueStoreChanged:(NSNotification *)note { [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]]]; else NSLog(@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]); - + NSDictionary* changedKeys = [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; for (NSString *key in changedKeys) { if ([key isEqualToString:iCloudUUIDKey]) { - + // Latest change wins [self updateLocalCopyOfiCloudUUID]; [self replaceiCloudStoreWithUUID:self.iCloudUUID]; @@ -760,18 +799,18 @@ - (void)mergeChanges:(NSNotification *)note { [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Ubiquitous store changes: %@", note.userInfo]]; else NSLog(@"Ubiquitous store changes: %@", note.userInfo); - + dispatch_async(persistentStorageQueue, ^{ NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; [moc performBlockAndWait:^{ - [moc mergeChangesFromContextDidSaveNotification:note]; + [moc mergeChangesFromContextDidSaveNotification:note]; }]; - + dispatch_async(dispatch_get_main_queue(), ^{ NSNotification* refreshNotification = [NSNotification notificationWithName: RefreshAllViewsNotificationKey object: self userInfo: [note userInfo]]; - + [[NSNotificationCenter defaultCenter] postNotification:refreshNotification]; }); }); @@ -784,11 +823,11 @@ - (void)registerForNotifications { name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification object: nil]; - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification + [[NSNotificationCenter defaultCenter]addObserver: self + selector: @selector(mergeChanges:) + name: NSPersistentStoreDidImportUbiquitousContentChangesNotification object: [self persistentStoreCoordinator]]; - + } - (void)removeNotifications { From a7b028ff80cd4768be686a9ef2067dfd31989ce0 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 4 Aug 2012 10:07:02 +0200 Subject: [PATCH 04/35] Rename notifications to make more general sense. --- iCloudStoreManager/UbiquityStoreManager.h | 4 ++-- iCloudStoreManager/UbiquityStoreManager.m | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 41d77fc..bda3094 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -30,8 +30,8 @@ #import #endif -extern NSString * const RefetchAllDatabaseDataNotificationKey; -extern NSString * const RefreshAllViewsNotificationKey; +extern NSString * const PersistentStoreDidChange; +extern NSString * const PersistentStoreDidMergeChanges; typedef enum { UbiquityStoreManagerErrorCauseDeleteStore, diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index f33ca30..0a96258 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -14,8 +14,8 @@ #define OS_Alert NSAlert #endif -NSString * const RefetchAllDatabaseDataNotificationKey = @"RefetchAllDatabaseData"; -NSString * const RefreshAllViewsNotificationKey = @"RefreshAllViews"; +NSString * const PersistentStoreDidChange = @"PersistentStoreDidChange"; +NSString * const PersistentStoreDidMergeChanges = @"PersistentStoreDidMergeChanges"; NSString *LocalUUIDKey = @"LocalUUIDKey"; NSString *iCloudUUIDKey = @"iCloudUUIDKey"; @@ -584,7 +584,7 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple } dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:RefetchAllDatabaseDataNotificationKey object:self userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:PersistentStoreDidChange object:self userInfo:nil]; [self didSwitchToiCloud:willUseiCloud]; }); }); @@ -807,7 +807,7 @@ - (void)mergeChanges:(NSNotification *)note { }]; dispatch_async(dispatch_get_main_queue(), ^{ - NSNotification* refreshNotification = [NSNotification notificationWithName: RefreshAllViewsNotificationKey + NSNotification* refreshNotification = [NSNotification notificationWithName:PersistentStoreDidMergeChanges object: self userInfo: [note userInfo]]; From 8c7894a983f1c1e0795a6e3d5ee7a223a5e01a54 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 24 Aug 2012 10:10:21 +0200 Subject: [PATCH 05/35] Avoid crashes related to a PSC with no store. [FIXED] If the PSC's stores disappear, make sure to re-open them. [FIXED] While the PSC is loading stores, mark it as not-ready, so the app knows not to use it. --- iCloudStoreManager/UbiquityStoreManager.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 0a96258..2f2a96f 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -424,10 +424,13 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (persistentStoreCoordinator__ == nil) { persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; + } + if (![persistentStoreCoordinator__.persistentStores count]) { NSString *uuid = (self.iCloudEnabled) ? self.localUUID : nil; [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:nil]; } + return persistentStoreCoordinator__; } @@ -521,6 +524,7 @@ - (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid comple // Do this asynchronously since if this is the first time this particular device is syncing with preexisting // iCloud content it may take a long long time to download + _isReady = NO; dispatch_async(persistentStorageQueue, ^{ NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableDictionary *options; From 20e2a1dec08fef1e99433f9a8b3a2310bb00717d Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 17 Jan 2013 00:31:03 -0500 Subject: [PATCH 06/35] Rewrite the whole ubiquityStoreManager for simplification. Removed all UI aspects. Removed localUUID More consistent naming Easier to use API Cleaner, more transparent, more modern and safer code. Uses (requires) iOS6's additions for working with iCloud. Handles more edge cases (iCloud account switches, store deletion, etc.) --- iCloudStoreManager/UbiquityStoreManager.h | 161 ++-- iCloudStoreManager/UbiquityStoreManager.m | 1067 ++++++++------------- 2 files changed, 468 insertions(+), 760 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index bda3094..e9cf8a6 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -7,98 +7,117 @@ // // UbiquityStoreManager manages the transfer of your SQL CoreData store from your local // application sandbox to iCloud. Even though it is not reinforced, UbiquityStoreManager -// is expected to be used as a singleton. This sample code is curently for iOS only. -// -// This class implements a very simple model. Once iCloud is seeded by data from a -// particular device, the iCloud store can never be re-seeded with fresh data. -// However, different devices can repeatedly switch between using their local store -// and the iCloud store. This is not necessarily a recommended practice but is implemented -// this way for testing and learning purposes. +// is expected to be used as a singleton. // // NSUbiquitousKeyValueStore is the mechanism used to discover which iCloud store to use. -// There may be better ways, but for now, that is what is being used. // -// Use the "Clear iCloud Data" button to reset iCloud data. This hard reset will propagate to all -// devices if the device's app is running. However, there may be a propagation delay of 20 sec. or more. -// or more. #import #import -#if TARGET_OS_IPHONE -#import -#else -#import -#endif -extern NSString * const PersistentStoreDidChange; -extern NSString * const PersistentStoreDidMergeChanges; +/** + * The store managed by the ubiquity manager's coordinator changed (eg. switched from iCloud to local). + */ +extern NSString *const UbiquityManagedStoreDidChangeNotification; +/** + * The store managed by the ubiquity manager's coordinator imported changes from iCloud (eg. another device saved changes to iCloud). + */ +extern NSString *const UbiquityManagedStoreDidImportChangesNotification; typedef enum { - UbiquityStoreManagerErrorCauseDeleteStore, - UbiquityStoreManagerErrorCauseDeleteLogs, - UbiquityStoreManagerErrorCauseCreateStorePath, - UbiquityStoreManagerErrorCauseClearStore, - UbiquityStoreManagerErrorCauseOpenLocalStore, - UbiquityStoreManagerErrorCauseOpenCloudStore, -} UbiquityStoreManagerErrorCause; + UbiquityStoreManagerErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. + UbiquityStoreManagerErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. + UbiquityStoreManagerErrorCauseClearStore, // Error occurred while removing the active store from the coordinator. + UbiquityStoreManagerErrorCauseOpenLocalStore, // Error occurred while opening the local store file. + UbiquityStoreManagerErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. + UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. +} UbiquityStoreManagerErrorCause; @class UbiquityStoreManager; -@protocol UbiquityStoreManagerDelegate +@protocol UbiquityStoreManagerDelegate + +@required - (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm; + @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)didSwitch; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error + cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; + @end -#if TARGET_OS_IPHONE -@interface UbiquityStoreManager : NSObject -#else @interface UbiquityStoreManager : NSObject -#endif -// The delegate confirms when a device has been switched to using either iCloud data or local data +// The delegate provides the managed object context to use and is informed of events in the ubiquity manager. @property (nonatomic, weak) id delegate; -// This property indicates whether the iCloud store or the local store is in use. To -// change state of this property, use useiCloudStore: method -@property (nonatomic, readonly) BOOL iCloudEnabled; - -// This property indicates when the persistentStoreCoordinator is ready. This property -// is always set immediately before the RefetchAllDatabaseDataNotification is sent. -@property (nonatomic, readonly) BOOL isReady; - -// Setting this property to YES is helpful for test purposes. It is highly recommended -// to set this to NO for production deployment -@property (nonatomic) BOOL hardResetEnabled; - -// Start by instantiating a UbiquityStoreManager with a managed object model. A valid localStoreURL -// is also required even if iCloud support is not currently enabled for this device. If it is enabled, -// it is required in case the user disables iCloud support for this device. If iCloud support is disabled -// after being initially enabled, the store on iCloud is NOT migrated back to the local device. -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)storeURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions; - -// Always use this method to instantiate or retrieve the main persistentStoreCoordinator. -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator; - -// If the user has decided to start using iCloud, call this method. And vice versa. -- (void)useiCloudStore:(BOOL)willUseiCloud alertUser:(BOOL)alertUser; - -// Reset iCloud data. Intended for test purposes only -- (void)hardResetCloudStorage; -- (void)hardResetLocalStorage; - -// Checks iCloud to ensure user has not deleted all iCloud data (nuke all use case). -// If the iCloud data has been deleted from within the Settings app or Mac System Preferences, -// iCloud will be disabled and the active store will be switched over to local store -- (void)checkiCloudStatus; - -// Array of all files and directorys in the ubiquity store. Useful for testing -- (NSArray *)fileList; - -// File URL for the currently selected store -- (NSURL *)currentStoreURL; +// Indicates whether the iCloud store or the local store is in use. +@property (nonatomic) BOOL cloudEnabled; + +// The coordinator provides access to this manager's active store. +@property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; + +/** + * Start managing an optionally ubiquitous store coordinator. Default settings will be used. + */ +- (id)init; + +/** Start managing an optionally ubiquitous store coordinator. + * @param contentName The name of the local and cloud stores that this manager will create. If nil, "UbiquityStore" will be used. + * @param model The managed object model the store should use. If nil, all the main bundle's models will be merged. + * @param localStoreURL The location where the non-ubiquitous (local) store should be kept. If nil, the local store will be put in the application support directory. + * @param containerIdentifier The identifier of the ubiquity container to use for the ubiquitous store. If nil, the entitlement's primary container identifier will be used. + * @param additionalStoreOptions Additional persistence options that the stores should be initialized with. + */ +- (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions; + +/** + * This will delete the local iCloud data for this application. There is no recovery. A new iCloud store will be initialized if enabled. + */ +- (void)nukeCloudContainer; + +/** + * This will delete the local store. There is no recovery. + */ +- (void)deleteLocalStore; + +/** + * This will delete the iCloud store. Theoretically, it should be rebuilt from the iCloud transaction logs. + * TODO: Verify claim. + */ +- (void)deleteCloudStore; + +/** +* @return URL to the active app's ubiquity container. +*/ +- (NSURL *)URLForCloudContainer; + +/** +* @return URL to the directory where we put cloud store databases for this app. +*/ +- (NSURL *)URLForCloudStoreDirectory; + +/** +* @return URL to the active cloud store's database. +*/ +- (NSURL *)URLForCloudStore; + +/** +* @return URL to the directory where we put cloud store transaction logs for this app. +*/ +- (NSURL *)URLForCloudContentDirectory; + +/** +* @return URL to the active cloud store's transaction logs. +*/ +- (NSURL *)URLForCloudContent; + +/** +* @return URL to the local store's database. +*/ +- (NSURL *)URLForLocalStore; @end diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 2f2a96f..5fdc57e 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -9,833 +9,522 @@ #import "UbiquityStoreManager.h" #if TARGET_OS_IPHONE -#define OS_Alert UIAlertView +#import #else -#define OS_Alert NSAlert + +#import + #endif -NSString * const PersistentStoreDidChange = @"PersistentStoreDidChange"; -NSString * const PersistentStoreDidMergeChanges = @"PersistentStoreDidMergeChanges"; - -NSString *LocalUUIDKey = @"LocalUUIDKey"; -NSString *iCloudUUIDKey = @"iCloudUUIDKey"; -NSString *iCloudEnabledKey = @"iCloudEnabledKey"; -NSString *DatabaseDirectoryName = @"Database.nosync"; -NSString *DataDirectoryName = @"Data"; - -@interface UbiquityStoreManager () { - NSDictionary *additionalStoreOptions__; - NSString *containerIdentifier__; - NSManagedObjectModel *model__; - NSPersistentStoreCoordinator *persistentStoreCoordinator__; - NSPersistentStore *persistentStore__; - NSURL *localStoreURL__; - OS_Alert *moveDataAlert; - OS_Alert *switchToiCloudAlert; - OS_Alert *switchToLocalAlert; - dispatch_queue_t persistentStorageQueue; -} +NSString *const UbiquityManagedStoreDidChangeNotification = @"UbiquityManagedStoreDidChangeNotification"; +NSString *const UbiquityManagedStoreDidImportChangesNotification = @"UbiquityManagedStoreDidImportChangesNotification"; +NSString *const StoreUUIDKey = @"StoreUUIDKey"; +NSString *const CloudEnabledKey = @"CloudEnabledKey"; +NSString *const CloudIdentityKey = @"CloudIdentityKey"; +NSString *const CloudStoreDirectory = @"CloudStore.nosync"; +NSString *const CloudLogsDirectory = @"CloudLogs"; + +@interface UbiquityStoreManager () + +@property (nonatomic, copy) NSString *contentName; +@property (nonatomic, strong) NSManagedObjectModel *model; +@property (nonatomic, copy) NSURL *localStoreURL; +@property (nonatomic, copy) NSString *containerIdentifier; +@property (nonatomic, copy) NSDictionary *additionalStoreOptions; -@property (nonatomic) NSString *localUUID; -@property (nonatomic) NSString *iCloudUUID; -@property (nonatomic, readwrite) BOOL iCloudEnabled; +@property (nonatomic) NSString *storeUUID; +@property (nonatomic) NSOperationQueue *persistentStorageQueue; +@property (nonatomic) BOOL loadingStore; -- (NSString *)freshUUID; -- (void)registerForNotifications; -- (void)removeNotifications; -- (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid completionBlock:(void (^)(BOOL usingiCloud))completionBlock; -- (void)checkiCloudStatus; @end @implementation UbiquityStoreManager +@synthesize persistentStoreCoordinator = _persistentStoreCoordinator; -@synthesize delegate; -@synthesize isReady = _isReady; -@synthesize hardResetEnabled = _hardResetEnabled; +- (id)init { -- (id)initWithManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)storeURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions { - self = [super init]; - if (self) { - additionalStoreOptions__ = additionalStoreOptions; - containerIdentifier__ = containerIdentifier; - model__ = model; - localStoreURL__ = storeURL; - persistentStorageQueue = dispatch_queue_create([@"PersistentStorageQueue" UTF8String], DISPATCH_QUEUE_SERIAL); + return self = [self initStoreNamed:nil withManagedObjectModel:nil localStoreURL:nil containerIdentifier:nil additionalStoreOptions:nil]; +} - // Start iCloud connection - [self updateLocalCopyOfiCloudUUID]; +- (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions { - [self checkiCloudStatus]; - [self registerForNotifications]; - } + if (!(self = [super init])) + return nil; - return self; -} + if (!localStoreURL) + localStoreURL = [[[self URLForApplicationContainer] URLByAppendingPathComponent:contentName] URLByAppendingPathExtension:@".sqlite"]; -- (void)dealloc { - [self removeNotifications]; - dispatch_release(persistentStorageQueue); -} + // Parameters + _contentName = contentName == nil? @"UbiquityStore": contentName; + _model = model == nil? [NSManagedObjectModel mergedModelFromBundles:nil]: model; + _localStoreURL = localStoreURL; + _containerIdentifier = containerIdentifier; + _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; -#pragma mark - File Handling - -- (NSURL *)iCloudStoreURLForUUID:(NSString *)uuid { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - NSString *storePath = [databaseContent stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", uuid]]; + // Private vars + _persistentStorageQueue = [NSOperationQueue new]; + [_persistentStorageQueue setName:[self className]]; - return [NSURL fileURLWithPath:storePath]; + return self; } -- (void)deleteStoreForUUID:(NSString *) uuid { - // TODO: -} +- (void)dealloc { -- (void)deleteStoreDirectory { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - - if ([fileManager fileExistsAtPath:databaseContent]) { - NSError *error = nil; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] - coordinateWritingItemAtURL:[NSURL fileURLWithPath:databaseContent] options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor: - ^(NSURL *newURL) { - NSError *error_ = nil; - if (![fileManager removeItemAtPath:databaseContent error:&error_]) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore - context:databaseContent]; - else - NSLog(@"Error deleting iCloud store: %@", error_); - } - }]; - - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore - context:databaseContent]; - else - NSLog(@"Error coordinating for deletion of iCloud store: %@", error); - } - } + [self clearStore]; } -- (void)createStoreDirectoryIfNecessary { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString *databaseContent = [[cloudURL path] stringByAppendingPathComponent:DatabaseDirectoryName]; - - if (![fileManager fileExistsAtPath:databaseContent]) { - NSError *error = nil; - [fileManager createDirectoryAtPath:databaseContent withIntermediateDirectories:YES attributes:nil error:&error]; +#pragma mark - File Handling - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:databaseContent]; - else - NSLog(@"Error creating database directory: %@", error); - } - } -} +- (NSURL *)URLForApplicationContainer { -- (NSURL *)transactionLogsURL { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - NSString* coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; + NSURL *applicationSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory + inDomains:NSUserDomainMask] lastObject]; - return [NSURL fileURLWithPath:coreDataCloudContent]; -} +#if TARGET_OS_IPHONE + // On iOS, each app is in a sandbox so we don't need to app-scope this directory. + return applicationSupportURL; +#else + // The directory is shared between all apps on the system so we need to scope it for the running app. + applicationSupportURL = [applicationSupportURL URLByAppendingPathComponent:[NSRunningApplication currentApplication].bundleIdentifier]; -- (void)deleteTransactionLogs { NSError *error = nil; - NSURL *transactionLogsURL = [self transactionLogsURL]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] - coordinateWritingItemAtURL:transactionLogsURL options:NSFileCoordinatorWritingForDeleting error:&error byAccessor: - ^(NSURL *newURL) { - NSError *error_ = nil; - if (![fileManager removeItemAtURL:transactionLogsURL error:&error_]) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteLogs - context:transactionLogsURL]; - else - NSLog(@"Error deleting transaction logs: %@", error_); - } - }]; - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs - context:transactionLogsURL]; - else - NSLog(@"Error coordinating for deletion of transaction logs: %@", error); - } -} + if (![[NSFileManager defaultManager] createDirectoryAtURL:applicationSupportURL + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:applicationSupportURL.path]; -- (NSURL *)transactionLogsURLForUUID:(NSString *)uuid { - return [[self transactionLogsURL] URLByAppendingPathComponent:uuid isDirectory:YES]; + return applicationSupportURL; +#endif } -- (void)deleteTransactionLogsForUUID:(NSString *)uuid { - if (uuid) { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; +- (NSURL *)URLForCloudContainer { - // Can only continue if iCloud is available - if (cloudURL) { - NSError *error = nil; - NSURL *transactionLogsURLForUUID = [self transactionLogsURLForUUID:uuid]; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] - coordinateWritingItemAtURL:transactionLogsURLForUUID options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor: - ^(NSURL *newURL) { - NSError *error_ = nil; - if (![fileManager removeItemAtURL:transactionLogsURLForUUID error:&error_]) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error_ cause:UbiquityStoreManagerErrorCauseDeleteLogs - context:transactionLogsURLForUUID]; - else - NSLog(@"Error deleting transaction logs for UUID: %@: %@", uuid, error_); - } - }]; - - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteLogs - context:transactionLogsURLForUUID]; - else - NSLog(@"Error coordinating for deletion of transaction logs for UUID: %@: %@", uuid, error); - } - } - } + return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:self.containerIdentifier]; } -#pragma mark - Message Strings +- (NSURL *)URLForCloudStoreDirectory { -// Subclass UbiquityStoreManager and override these methods if you want to customize these messages - -- (NSString *)moveDataToiCloudTitle { - return @"Move Data to iCloud"; + // We put the database in the ubiquity container with a .nosync extension (must not be synced by iCloud), + // so that its presence is tied closely to whether iCloud is enabled or not on the device + // and the user can delete the store by deleting his iCloud data for the app from Settings. + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudStoreDirectory]; } -- (NSString *)moveDataToiCloudMessage { - return @"Your data is about to be moved to iCloud. If you prefer to start using iCloud with data from a different device, tap Cancel and enable iCloud from that other device."; -} +- (NSURL *)URLForCloudStore { -- (NSString *)switchDataToiCloudTitle { - return @"iCloud Data"; + // Our cloud store is in the cloud store databases directory and is identified by the active storeUUID. + return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:self.storeUUID] URLByAppendingPathExtension:@"sqlite"]; } -- (NSString *)switchDataToiCloudMessage { - return @"Would you like to switch to using data from iCloud?"; -} +- (NSURL *)URLForCloudContentDirectory { -- (NSString *)tryLaterTitle { - return @"iCloud Not Available"; + // The transaction logs are in the ubiquity container and are synced by iCloud. + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudLogsDirectory]; } -- (NSString *)tryLaterMessage { - return @"iCloud is not currently available. Please try again later."; +- (NSURL *)URLForCloudContent { + + // Our cloud store's logs are in the cloud store transaction logs directory and is identified by the active storeUUID. + return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:self.storeUUID]; } +- (NSURL *)URLForLocalStore { -- (NSString *)switchToLocalDataTitle { - return @"Stop Using iCloud"; + return self.localStoreURL; } -- (NSString *)switchToLocalDataMessage { - return @"If you stop using iCloud you will switch to using local data on this device only. Your local data is completely separate from iCloud. Any changes you make will not be be synchronized with iCloud."; -} -#pragma mark - UIAlertView - -- (void)alertView:(OS_Alert *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { - if (alertView == moveDataAlert) { - if (buttonIndex == 1) { - // Move the data from the local store to the iCloud store - [self setupCloudStorageWithUUID:[self freshUUID]]; - } - else { - [self didSwitchToiCloud:NO]; - } - } - - if (alertView == switchToiCloudAlert) { - if (buttonIndex == 1) { - // Switch to using data from iCloud - [self useCloudStorage]; - } - else { - [self didSwitchToiCloud:NO]; - } - } - - if (alertView == switchToLocalAlert) { - if (buttonIndex == 1) { - // Switch to using data from iCloud - [self useLocalStorage]; - } - else { - [self didSwitchToiCloud:YES]; - } - } -} +#pragma mark - Utilities -- (void)moveDataToiCloudAlert { -#if TARGET_OS_IPHONE - moveDataAlert = [[UIAlertView alloc] initWithTitle: [self moveDataToiCloudTitle] - message: [self moveDataToiCloudMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Move Data", nil]; - [moveDataAlert show]; -#else - moveDataAlert = [NSAlert alertWithMessageText:[self moveDataToiCloudTitle] - defaultButton:@"Move Data" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self moveDataToiCloudMessage]]; - NSInteger button = [moveDataAlert runModal]; - [self alertView:moveDataAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} +- (void)log:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2) { -- (void)switchToiCloudDataAlert { -#if TARGET_OS_IPHONE - switchToiCloudAlert = [[UIAlertView alloc] initWithTitle: [self switchDataToiCloudTitle] - message: [self switchDataToiCloudMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Use iCloud", nil]; - [switchToiCloudAlert show]; -#else - switchToiCloudAlert = [NSAlert alertWithMessageText:[self switchDataToiCloudTitle] - defaultButton:@"Use iCloud" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self switchDataToiCloudMessage]]; - NSInteger button = [switchToiCloudAlert runModal]; - [self alertView:switchToiCloudAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} + va_list argList; + va_start(argList, format); + NSString *message = [[NSString alloc] initWithFormat:format arguments:argList]; + va_end(argList); -- (void)tryLaterAlert { -#if TARGET_OS_IPHONE - UIAlertView *alert = [[UIAlertView alloc] initWithTitle: [self tryLaterTitle] - message: [self tryLaterMessage] - delegate: nil - cancelButtonTitle: @"Done" - otherButtonTitles: nil]; - [alert show]; -#else - NSAlert *alert = [NSAlert alertWithMessageText:[self tryLaterTitle] - defaultButton:@"Cancel" - alternateButton:nil - otherButton:nil - informativeTextWithFormat:[self tryLaterMessage]]; - [alert runModal]; -#endif + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) + [self.delegate ubiquityStoreManager:self log:message]; + else + NSLog(@"UbiquityStoreManager: %@", message); } -- (void)switchToLocalDataAlert { -#if TARGET_OS_IPHONE - switchToLocalAlert = [[UIAlertView alloc] initWithTitle: [self switchToLocalDataTitle] - message: [self switchToLocalDataMessage] - delegate: self - cancelButtonTitle: @"Cancel" - otherButtonTitles: @"Continue", nil]; - [switchToLocalAlert show]; -#else - switchToLocalAlert = [NSAlert alertWithMessageText:[self switchToLocalDataTitle] - defaultButton:@"Continue" - alternateButton:@"Cancel" - otherButton:nil - informativeTextWithFormat:[self switchToLocalDataMessage]]; - NSInteger button = [switchToLocalAlert runModal]; - [self alertView:switchToLocalAlert didDismissWithButtonIndex:button == NSAlertDefaultReturn? 1: 0]; -#endif -} +- (void)error:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context { -#pragma mark - Test Methods - -- (void)hardResetLocalStorage { - if (_hardResetEnabled) { - NSError *error; - [[NSFileManager defaultManager] removeItemAtURL:localStoreURL__ error:&error]; - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:localStoreURL__]; - else - NSLog(@"Error deleting local store: %@", error); - } - } + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) + [self.delegate ubiquityStoreManager:self didEncounterError:error cause:cause context:context]; + else + [self log:@"error: %@, cause: %u, context: %@", error, cause, context]; } -- (void)hardResetCloudStorage { - if (_hardResetEnabled) { - [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { - [self deleteStoreDirectory]; - [self deleteTransactionLogs]; - - // Setting iCloudUUID to nil will propagate to all other devices, - // and automatically force them to switch over to their local stores +#pragma mark - Store Management - self.iCloudUUID = nil; - self.localUUID = nil; - self.iCloudEnabled = NO; - }]; - } -} +- (void)clearStore { -- (NSArray *)fileList { - NSArray *fileList = nil; + // Remove store observers. + [NSFileCoordinator removeFilePresenter:self]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; + // Remove the store from the coordinator. + NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; + BOOL pscLockedByUs = [psc tryLock]; + NSError *error = nil; + BOOL failed = NO; - if (cloudURL) - fileList = [fileManager subpathsAtPath:[cloudURL path]]; + for (NSPersistentStore *store in psc.persistentStores) + if (![psc removePersistentStore:store error:&error]) { + failed = YES; + [self error:error cause:UbiquityStoreManagerErrorCauseClearStore context:store]; + } - return fileList; + if (pscLockedByUs) + [psc unlock]; + if (failed) + // Try to recover by throwing out the PSC. + _persistentStoreCoordinator = nil; } -#pragma mark - Persistent Store Management +- (void)loadStore { -- (void)clearPersistentStore { - if (persistentStore__) { - NSError *error = nil; - [persistentStoreCoordinator__ removePersistentStore:persistentStore__ error:&error]; - persistentStore__ = nil; + @synchronized (self) { + if (self.loadingStore) + return; + self.loadingStore = YES; + } - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseClearStore context:persistentStore__]; - else - NSLog(@"Error removing persistent store: %@", error); + if (!self.cloudEnabled) { + @try { + // Load local store if iCloud is disabled. + NSError *error = nil; + NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + + // Add local store to PSC. + [self.persistentStoreCoordinator lock]; + [self clearStore]; + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:[self URLForLocalStore] + options:localStoreOptions + error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; + [self observeStore]; + } + @finally { + [self.persistentStoreCoordinator unlock]; + self.loadingStore = NO; } - } -} -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { + [self log:@"iCloud disabled. Loaded local store."]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) + [self.delegate ubiquityStoreManager:self didSwitchToiCloud:NO]; + }); - if (persistentStoreCoordinator__ == nil) { - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; + return; } - if (![persistentStoreCoordinator__.persistentStores count]) { - NSString *uuid = (self.iCloudEnabled) ? self.localUUID : nil; - [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:nil]; - } + // Otherwise, load iCloud store asynchronously (init of iCloud may take some time). + [self.persistentStorageQueue addOperationWithBlock:^{ + @try { + if (![self URLForCloudContainer]) { + // iCloud is not enabled on this device. Disable iCloud in the app (will cause a re-load using the local store). + // TODO: Notify user? + self.loadingStore = NO; + self.cloudEnabled = NO; + return; + } - return persistentStoreCoordinator__; -} + // Migrate the local store to a new cloud store when there is no cloud store yet. + BOOL migrateLocalToCloud = NO; + if (!self.storeUUID) { + self.storeUUID = [[NSUUID UUID] UUIDString]; -- (void)migrateToiCloud:(BOOL)migrate persistentStoreCoordinator:(NSPersistentStoreCoordinator *)psc with:(NSString *)uuid { - NSMutableDictionary *options; - - NSAssert([[psc persistentStores] count] == 0, @"There were more persistent stores than expected"); - - [self createStoreDirectoryIfNecessary]; - - NSError *error = nil; - NSURL *transactionLogsURL = [self transactionLogsURLForUUID:uuid]; - - options = [NSMutableDictionary dictionaryWithObjectsAndKeys: - uuid, NSPersistentStoreUbiquitousContentNameKey, - transactionLogsURL, NSPersistentStoreUbiquitousContentURLKey, - [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, - [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, - nil]; - [options addEntriesFromDictionary:additionalStoreOptions__]; - - [psc lock]; - - NSURL *cloudStoreURL = [self iCloudStoreURLForUUID:uuid]; - if (migrate) { - // Clear old registered notifcations. This was required to address an exception that occurs when using - // a persistent store on iCloud setup by another device (Object's persistent store is not reachable - // from this NSManagedObjectContext's coordinator) - [[NSNotificationCenter defaultCenter] removeObserver: self - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: psc]; - - // Add the store to migrate - NSPersistentStore * migratedStore = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: localStoreURL__ - options: additionalStoreOptions__ - error: &error]; - - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; - else - NSLog(@"Prepping migrated store error: %@", error); - } + if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) + migrateLocalToCloud = YES; + } - error = nil; - persistentStore__ = [psc migratePersistentStore: migratedStore - toURL: cloudStoreURL - options: options - withType: NSSQLiteStoreType - error: &error]; - - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: psc]; - } - else { - persistentStore__ = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: cloudStoreURL - options: options - error: &error]; - } - [psc unlock]; - - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL]; - else { - NSLog(@"Persistent store error: %@", error); - NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); + // Create the path to the cloud store. + NSError *error = nil; + if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + + // Add cloud store to PSC. + NSURL *cloudStoreURL = [self URLForCloudStore]; + NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + self.contentName, NSPersistentStoreUbiquitousContentNameKey, + [self URLForCloudContent], NSPersistentStoreUbiquitousContentURLKey, + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + NSMutableDictionary *migratingStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSReadOnlyPersistentStoreOption, + nil]; + [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [migratingStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + + [self.persistentStoreCoordinator lock]; + [self clearStore]; + + if (migrateLocalToCloud) { + // First add the local store, then migrate it to the cloud store. + [self log:@"Migrating local store to new cloud store."]; + + // Add the store to migrate + NSPersistentStore *migratingStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:[self URLForLocalStore] + options:migratingStoreOptions + error:&error]; + + if (!migratingStore) + [self error:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; + + else if (![self.persistentStoreCoordinator migratePersistentStore:migratingStore + toURL:cloudStoreURL + options:cloudStoreOptions + withType:NSSQLiteStoreType + error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + } + // Not migrating, just add the existing cloud store. + else if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + [self observeStore]; } - } + @finally { + [self.persistentStoreCoordinator unlock]; + self.loadingStore = NO; + } + + [self log:@"iCloud enabled. Loaded cloud store."]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) + [self.delegate ubiquityStoreManager:self didSwitchToiCloud:YES]; + }); + }]; } -- (void)migrate:(BOOL)migrate andUseCloudStorageWithUUID:(NSString *)uuid completionBlock:(void (^)(BOOL usingiCloud))completionBlock { +- (void)observeStore { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Setting up store with UUID: %@", uuid]]; - else - NSLog(@"Setting up store with UUID: %@", uuid); - BOOL willUseiCloud = (uuid != nil); + if (self.cloudEnabled) { + [NSFileCoordinator addFilePresenter:self]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) + name:NSPersistentStoreDidImportUbiquitousContentChangesNotification + object:self.persistentStoreCoordinator]; + } - // TODO: Check for use case where user checks out of one iCloud account, and logs into another! + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) + name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cloudStoreChanged:) + name:NSUbiquityIdentityDidChangeNotification + object:nil]; +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +#else + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:NSApplicationDidBecomeActiveNotification + object:nil]; +#endif +} - // TODO: Test deletion from Settings App -> Manage Data -> Delete Data (nuke option) +- (void)nukeCloudContainer { - NSPersistentStoreCoordinator* psc = persistentStoreCoordinator__; + self.storeUUID = nil; - // Do this asynchronously since if this is the first time this particular device is syncing with preexisting - // iCloud content it may take a long long time to download - _isReady = NO; - dispatch_async(persistentStorageQueue, ^{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSMutableDictionary *options; + NSURL *cloudContainerURL = [self URLForCloudContainer]; + if (cloudContainerURL && [[NSFileManager defaultManager] fileExistsAtPath:cloudContainerURL.path]) { + [self.persistentStoreCoordinator lock]; + [self clearStore]; - // Clear previous persistentStore - [self clearPersistentStore]; + NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + for (NSString *subPath in [[NSFileManager defaultManager] subpathsAtPath:cloudContainerURL.path]) { + NSError *error = nil; + [coordinator coordinateWritingItemAtURL:[NSURL fileURLWithPath:subPath] options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + }]; - NSString* coreDataCloudContent = nil; - if (willUseiCloud) { - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; - coreDataCloudContent = [[cloudURL path] stringByAppendingPathComponent:DataDirectoryName]; + if (error) + [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:subPath]; } - BOOL usingiCloud = ([coreDataCloudContent length] != 0); - - if (usingiCloud) { - // iCloud is available - [self migrateToiCloud:migrate persistentStoreCoordinator:psc with:uuid]; - } - else { - // iCloud is not available - options = [NSMutableDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, - [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, - nil]; - [options addEntriesFromDictionary:additionalStoreOptions__]; - - [psc lock]; - NSError *error = nil; - persistentStore__ = [psc addPersistentStoreWithType: NSSQLiteStoreType - configuration: nil - URL: localStoreURL__ - options: options - error: &error]; - if (error) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) - [self.delegate ubiquityStoreManager:self didEncounterError:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL__]; - else - NSLog(@"Persistent store error: %@", error); - } - - [psc unlock]; - } + [self.persistentStoreCoordinator unlock]; + [self loadStore]; + } +} - if (![[psc persistentStores] count]) - return; +- (void)deleteLocalStore { - _isReady = YES; + [self clearStore]; + NSError *error = nil; + NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + + [coordinator coordinateWritingItemAtURL:[self URLForLocalStore] options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor:^(NSURL *newURL) { + NSError *error_ = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + }]; + + if (error) + [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:[self URLForLocalStore].path]; - NSAssert([[psc persistentStores] count] == 1, @"Not the expected number of persistent stores"); + [self loadStore]; +} - NSString *usingiCloudString = (usingiCloud) ? @" using iCloud!" : @"!"; +- (void)deleteCloudStore { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Asynchronously added persistent store%@", usingiCloudString]]; - else - NSLog(@"Asynchronously added persistent store%@", usingiCloudString); + [self clearStore]; + NSError *error = nil; + NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - if (completionBlock) { - completionBlock(usingiCloud); - } + NSURL *cloudStoreURL = [self URLForCloudStore]; + [coordinator coordinateWritingItemAtURL:cloudStoreURL options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor:^(NSURL *newURL) { + NSError *error_ = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; + }]; + + if (error) + [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:PersistentStoreDidChange object:self userInfo:nil]; - [self didSwitchToiCloud:willUseiCloud]; - }); - }); + [self loadStore]; } -- (void)useCloudStorage { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; +#pragma mark - Properties - [self migrate:NO andUseCloudStorageWithUUID:self.iCloudUUID completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = self.iCloudUUID; - self.iCloudEnabled = YES; - } - }]; -} +- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { -- (void)useLocalStorage { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; + if (!_persistentStoreCoordinator) { + _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - [self migrate:NO andUseCloudStorageWithUUID:nil completionBlock:^(BOOL usingiCloud) { - self.localUUID = nil; - self.iCloudEnabled = NO; - }]; -} + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) + name:NSPersistentStoreDidImportUbiquitousContentChangesNotification + object:_persistentStoreCoordinator]; + } -- (void)setupCloudStorageWithUUID:(NSString *)uuid { + if (![_persistentStoreCoordinator.persistentStores count]) + [self loadStore]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Setting up iCloud data with new UUID: %@", uuid]]; - else - NSLog(@"Setting up iCloud data with new UUID: %@", uuid); - - [self migrate:YES andUseCloudStorageWithUUID:uuid completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = uuid; - self.iCloudUUID = uuid; - self.iCloudEnabled = YES; - } - }]; + return _persistentStoreCoordinator; } -- (void)replaceiCloudStoreWithUUID:(NSString *)uuid { - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - [self migrate:NO andUseCloudStorageWithUUID:uuid completionBlock:^(BOOL usingiCloud) { - if (usingiCloud) { - self.localUUID = uuid; - self.iCloudEnabled = YES; - } - else { - if (_hardResetEnabled) { - // Hard reset has occurred. Delete database and transaction logs - [self deleteStoreDirectory]; - [self deleteTransactionLogs]; - } - - self.localUUID = nil; - self.iCloudEnabled = NO; - } - }]; -} +- (BOOL)cloudEnabled { -- (NSURL *)currentStoreURL { - if (self.iCloudEnabled) - return [self iCloudStoreURLForUUID:self.iCloudUUID]; - else - return localStoreURL__; + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + return [local boolForKey:CloudEnabledKey]; } -#pragma mark - Top Level Methods +- (void)setCloudEnabled:(BOOL)enabled { -- (void)checkiCloudStatus { - if (self.iCloudEnabled) { - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *cloudURL = [fileManager URLForUbiquityContainerIdentifier:containerIdentifier__]; + if (self.cloudEnabled == enabled) + // No change, do nothing to avoid a needless store reload. + return; - // If we have only one file/directory (Documents directory), then iCloud data has been deleted by user - if ((cloudURL == nil) || [[self fileList] count] < 2) - [self useLocalStorage]; - } + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + [local setBool:enabled forKey:CloudEnabledKey]; + [self loadStore]; } -- (void)useiCloudStore:(BOOL)willUseiCloud alertUser:(BOOL)alertUser { - // To provide the option of using iCloud immediately upon first running of an app, - // make sure a persistentStoreCoordinator exists. - - if (persistentStoreCoordinator__ == nil) - persistentStoreCoordinator__ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model__]; - - if (willUseiCloud) { - if (!self.iCloudEnabled) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - - // If an iCloud store already exists, ask the user if they want to switch over to iCloud - if (cloud) { - if (self.iCloudUUID) { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self switchToiCloudDataAlert]; - else - [self useCloudStorage]; - } - else { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self moveDataToiCloudAlert]; - else - [self setupCloudStorageWithUUID:[self freshUUID]]; - } - } - else if (alertUser) { - [self tryLaterAlert]; - } - } - } - else { - if (self.iCloudEnabled) { - if (alertUser && [localStoreURL__ checkResourceIsReachableAndReturnError:nil]) - [self switchToLocalDataAlert]; - else - [self useLocalStorage]; - } - } -} +- (NSString *)storeUUID { -- (void)didSwitchToiCloud:(BOOL)didSwitch { - if ([delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) { - [delegate ubiquityStoreManager:self didSwitchToiCloud:didSwitch]; - } + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + return [cloud objectForKey:StoreUUIDKey]; } -#pragma mark - Properties +- (void)setStoreUUID:(NSString *)storeUUID { -- (BOOL)iCloudEnabled { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local boolForKey:iCloudEnabledKey]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setObject:storeUUID forKey:StoreUUIDKey]; + [cloud synchronize]; } -- (void)setICloudEnabled:(BOOL)enabled { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setBool:enabled forKey:iCloudEnabledKey]; -} +#pragma mark - NSFilePresenter -- (NSString *)freshUUID { - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); +- (NSURL *)presentedItemURL { - CFRelease(uuidRef); + if (self.cloudEnabled) + return [self URLForCloudStore]; - return (__bridge_transfer NSString *)uuidStringRef; + return [self URLForLocalStore]; } -- (void)setLocalUUID:(NSString *)uuid { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setObject:uuid forKey:LocalUUIDKey]; - [local synchronize]; -} +- (NSOperationQueue *)presentedItemOperationQueue { -- (NSString *)localUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local objectForKey:LocalUUIDKey]; + return self.persistentStorageQueue; } -- (void)setICloudUUID:(NSString *)uuid { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud setObject:uuid forKey:iCloudUUIDKey]; - [cloud synchronize]; +- (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setObject:uuid forKey:iCloudUUIDKey]; - [local synchronize]; + // Active store file was deleted. + [self cloudStoreChanged:nil]; + completionHandler(nil); } -- (NSString *)iCloudUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - return [local objectForKey:iCloudUUIDKey]; -} - -- (void)updateLocalCopyOfiCloudUUID { - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [local setObject:[cloud objectForKey:iCloudUUIDKey] forKey:iCloudUUIDKey]; - [local synchronize]; -} -#pragma mark - KeyValueStore Notification +#pragma mark - Notifications -- (void)keyValueStoreChanged:(NSNotification *)note { +- (void)applicationDidBecomeActive:(NSNotification *)note { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]]]; - else - NSLog(@"KeyValueStore changed: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]); + // Check for account changes. + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + id lastIdentityToken = [local objectForKey:CloudIdentityKey]; + id currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; + if (![lastIdentityToken isEqual:currentIdentityToken]) { + [self cloudStoreChanged:nil]; + return; + } +} - NSDictionary* changedKeys = [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; - for (NSString *key in changedKeys) { - if ([key isEqualToString:iCloudUUIDKey]) { +- (void)keyValueStoreChanged:(NSNotification *)note { - // Latest change wins - [self updateLocalCopyOfiCloudUUID]; - [self replaceiCloudStoreWithUUID:self.iCloudUUID]; - } - } + if ([(NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey] containsObject:StoreUUIDKey]) + [self cloudStoreChanged:nil]; } -#pragma mark - Notifications +- (void)cloudStoreChanged:(NSNotification *)note { -- (void)mergeChanges:(NSNotification *)note { + // Update the identity token in case it changed. + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + id identityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; + [local setObject:identityToken forKey:CloudIdentityKey]; + [local synchronize]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:log:)]) - [self.delegate ubiquityStoreManager:self log:[NSString stringWithFormat:@"Ubiquitous store changes: %@", note.userInfo]]; - else - NSLog(@"Ubiquitous store changes: %@", note.userInfo); - - dispatch_async(persistentStorageQueue, ^{ - NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; - [moc performBlockAndWait:^{ - [moc mergeChangesFromContextDidSaveNotification:note]; - }]; - - dispatch_async(dispatch_get_main_queue(), ^{ - NSNotification* refreshNotification = [NSNotification notificationWithName:PersistentStoreDidMergeChanges - object: self - userInfo: [note userInfo]]; - - [[NSNotificationCenter defaultCenter] postNotification:refreshNotification]; - }); - }); + // Reload the store. + [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, identityToken]; + [self loadStore]; } +- (void)mergeChanges:(NSNotification *)note { -- (void)registerForNotifications { - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(keyValueStoreChanged:) - name: NSUbiquitousKeyValueStoreDidChangeExternallyNotification - object: nil]; - - [[NSNotificationCenter defaultCenter]addObserver: self - selector: @selector(mergeChanges:) - name: NSPersistentStoreDidImportUbiquitousContentChangesNotification - object: [self persistentStoreCoordinator]]; - -} + [self log:@"Cloud store updates:\n%@", note.userInfo]; + [self.persistentStorageQueue addOperationWithBlock:^{ + NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; + [moc performBlockAndWait:^{ + [moc mergeChangesFromContextDidSaveNotification:note]; + }]; -- (void)removeNotifications { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self + userInfo:[note userInfo]]; + }); + }]; } @end From 8faba0d3c35d471005c45280dc2670bc811e08c6 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 25 Jan 2013 01:02:33 -0500 Subject: [PATCH 07/35] Fix the initial storeUUID determination. [FIXED] Now using a tentativeStoreUUID to store a new random UUID that will be assigned to the storeUUID as soon as the store has been successfully created or migrated with it. --- iCloudStoreManager/UbiquityStoreManager.m | 85 ++++++++++++++++------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 5fdc57e..5d3ccfc 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -31,10 +31,11 @@ @interface UbiquityStoreManager () @property (nonatomic, copy) NSURL *localStoreURL; @property (nonatomic, copy) NSString *containerIdentifier; @property (nonatomic, copy) NSDictionary *additionalStoreOptions; - -@property (nonatomic) NSString *storeUUID; -@property (nonatomic) NSOperationQueue *persistentStorageQueue; +@property (nonatomic, readonly) NSString *storeUUID; +@property (nonatomic, strong) NSString *tentativeStoreUUID; +@property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; @property (nonatomic) BOOL loadingStore; +@property (nonatomic) BOOL migrateLocalToCloud; @end @@ -53,12 +54,13 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb if (!(self = [super init])) return nil; - if (!localStoreURL) - localStoreURL = [[[self URLForApplicationContainer] URLByAppendingPathComponent:contentName] URLByAppendingPathExtension:@".sqlite"]; - // Parameters _contentName = contentName == nil? @"UbiquityStore": contentName; _model = model == nil? [NSManagedObjectModel mergedModelFromBundles:nil]: model; + if (!localStoreURL) + localStoreURL = [[[self URLForApplicationContainer] + URLByAppendingPathComponent:self.contentName isDirectory:NO] + URLByAppendingPathExtension:@".sqlite"]; _localStoreURL = localStoreURL; _containerIdentifier = containerIdentifier; _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; @@ -87,7 +89,7 @@ - (NSURL *)URLForApplicationContainer { return applicationSupportURL; #else // The directory is shared between all apps on the system so we need to scope it for the running app. - applicationSupportURL = [applicationSupportURL URLByAppendingPathComponent:[NSRunningApplication currentApplication].bundleIdentifier]; + applicationSupportURL = [applicationSupportURL URLByAppendingPathComponent:[NSRunningApplication currentApplication].bundleIdentifier isDirectory:YES]; NSError *error = nil; if (![[NSFileManager defaultManager] createDirectoryAtURL:applicationSupportURL @@ -108,25 +110,29 @@ - (NSURL *)URLForCloudStoreDirectory { // We put the database in the ubiquity container with a .nosync extension (must not be synced by iCloud), // so that its presence is tied closely to whether iCloud is enabled or not on the device // and the user can delete the store by deleting his iCloud data for the app from Settings. - return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudStoreDirectory]; + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudStoreDirectory isDirectory:YES]; } - (NSURL *)URLForCloudStore { // Our cloud store is in the cloud store databases directory and is identified by the active storeUUID. - return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:self.storeUUID] URLByAppendingPathExtension:@"sqlite"]; + NSString *uuid = self.storeUUID; + NSAssert(uuid, @"No storeUUID set."); + return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:uuid isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; } - (NSURL *)URLForCloudContentDirectory { // The transaction logs are in the ubiquity container and are synced by iCloud. - return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudLogsDirectory]; + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudLogsDirectory isDirectory:YES]; } - (NSURL *)URLForCloudContent { // Our cloud store's logs are in the cloud store transaction logs directory and is identified by the active storeUUID. - return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:self.storeUUID]; + NSString *uuid = self.storeUUID; + NSAssert(uuid, @"No storeUUID set."); + return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:uuid isDirectory:YES]; } - (NSURL *)URLForLocalStore { @@ -239,13 +245,16 @@ - (void)loadStore { return; } - // Migrate the local store to a new cloud store when there is no cloud store yet. - BOOL migrateLocalToCloud = NO; - if (!self.storeUUID) { - self.storeUUID = [[NSUUID UUID] UUIDString]; - - if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) - migrateLocalToCloud = YES; + // If a migration is requested but no local store is present, don't migrate. + if (self.migrateLocalToCloud) { + if (![[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) { + [self log:@"Cannot migrate local store to cloud: local store does not exist."]; + self.migrateLocalToCloud = NO; + } + else if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) { + [self log:@"Cannot migrate local store to cloud: cloud store already exists."]; + self.migrateLocalToCloud = NO; + } } // Create the path to the cloud store. @@ -271,7 +280,7 @@ - (void)loadStore { [self.persistentStoreCoordinator lock]; [self clearStore]; - if (migrateLocalToCloud) { + if (self.migrateLocalToCloud) { // First add the local store, then migrate it to the cloud store. [self log:@"Migrating local store to new cloud store."]; @@ -291,12 +300,14 @@ - (void)loadStore { error:&error]) [self error:error cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; } - // Not migrating, just add the existing cloud store. + // Not migrating, just add the cloud store. else if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:cloudStoreURL options:cloudStoreOptions error:&error]) [self error:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + + [self confirmTentativeStoreUUID]; [self observeStore]; } @finally { @@ -341,13 +352,12 @@ - (void)observeStore { - (void)nukeCloudContainer { - self.storeUUID = nil; - NSURL *cloudContainerURL = [self URLForCloudContainer]; if (cloudContainerURL && [[NSFileManager defaultManager] fileExistsAtPath:cloudContainerURL.path]) { [self.persistentStoreCoordinator lock]; [self clearStore]; + // Delete the contents of the cloud container. NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; for (NSString *subPath in [[NSFileManager defaultManager] subpathsAtPath:cloudContainerURL.path]) { NSError *error = nil; @@ -363,6 +373,11 @@ - (void)nukeCloudContainer { [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:subPath]; } + // Unset the storeUUID so a new one will be created. + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + [self.persistentStoreCoordinator unlock]; [self loadStore]; } @@ -445,14 +460,30 @@ - (void)setCloudEnabled:(BOOL)enabled { - (NSString *)storeUUID { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - return [cloud objectForKey:StoreUUIDKey]; + NSString *storeUUID = [cloud objectForKey:StoreUUIDKey]; + + // If no storeUUID is set yet, create a new storeUUID and return that as long as no storeUUID is set yet. + // When the migration to the new storeUUID is successful, we update the iCloud's KVS with a call to -setStoreUUID. + if (!storeUUID) { + if (!self.tentativeStoreUUID) + self.tentativeStoreUUID = [[NSUUID UUID] UUIDString]; + storeUUID = self.tentativeStoreUUID; + } + + return storeUUID; } -- (void)setStoreUUID:(NSString *)storeUUID { +/** + * When a tentativeStoreUUID is set, this operation confirms it and writes it as the new storeUUID to the iCloud KVS. + */ +- (void)confirmTentativeStoreUUID { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud setObject:storeUUID forKey:StoreUUIDKey]; - [cloud synchronize]; + if (self.tentativeStoreUUID) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; + [cloud synchronize]; + self.tentativeStoreUUID = nil; + } } #pragma mark - NSFilePresenter From b748092775b2ee07c4c494434b43c72fb589ab67 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 27 Jan 2013 00:52:04 -0500 Subject: [PATCH 08/35] Naming consistency improvement and compiler warning fix. [IMPROVED] Small naming improvements to be more consistent with the new API. [FIXED] A warning with regards to className not being a known selector. --- iCloudStoreManager/UbiquityStoreManager.h | 2 +- iCloudStoreManager/UbiquityStoreManager.m | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index e9cf8a6..0f27d6b 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -41,7 +41,7 @@ typedef enum { - (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm; @optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)didSwitch; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled; - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 5d3ccfc..dbc440f 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -67,7 +67,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb // Private vars _persistentStorageQueue = [NSOperationQueue new]; - [_persistentStorageQueue setName:[self className]]; + [_persistentStorageQueue setName:NSStringFromClass([self class])]; return self; } @@ -227,8 +227,8 @@ - (void)loadStore { [self log:@"iCloud disabled. Loaded local store."]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) - [self.delegate ubiquityStoreManager:self didSwitchToiCloud:NO]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) + [self.delegate ubiquityStoreManager:self didSwitchToCloud:NO]; }); return; @@ -318,8 +318,8 @@ - (void)loadStore { [self log:@"iCloud enabled. Loaded cloud store."]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToiCloud:)]) - [self.delegate ubiquityStoreManager:self didSwitchToiCloud:YES]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) + [self.delegate ubiquityStoreManager:self didSwitchToCloud:YES]; }); }]; } From 39ac68e0fb34f0c6e2f455f132b56fd158630ee7 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 27 Jan 2013 20:10:04 -0500 Subject: [PATCH 09/35] Fixes to local store location and migration. [FIXED] Duplicate dot when adding extension sqlite to local store name. [FIXED] Don't try to migrate when the cloud store is not tentative (another device created a cloud store already). [ADDED] API for applications to test whether it's safe to seed the cloud store (if they wish to do so manually). [FIXED] Create the directory for the local store before opening it in case it doesn't exist yet. --- iCloudStoreManager/UbiquityStoreManager.h | 10 ++++++ iCloudStoreManager/UbiquityStoreManager.m | 43 ++++++++++++++--------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 0f27d6b..0332f6a 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -90,6 +90,11 @@ typedef enum { */ - (void)deleteCloudStore; +/** +* Determine whether it's safe to seed the cloud store with a local store. +*/ +- (BOOL)cloudSafeForSeeding; + /** * @return URL to the active app's ubiquity container. */ @@ -115,6 +120,11 @@ typedef enum { */ - (NSURL *)URLForCloudContent; +/** + * @return URL to the directory where we put the local store database for this app. + */ +- (NSURL *)URLForLocalStoreDirectory; + /** * @return URL to the local store's database. */ diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index dbc440f..59c8f9c 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -35,7 +35,6 @@ @interface UbiquityStoreManager () @property (nonatomic, strong) NSString *tentativeStoreUUID; @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; @property (nonatomic) BOOL loadingStore; -@property (nonatomic) BOOL migrateLocalToCloud; @end @@ -60,7 +59,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb if (!localStoreURL) localStoreURL = [[[self URLForApplicationContainer] URLByAppendingPathComponent:self.contentName isDirectory:NO] - URLByAppendingPathExtension:@".sqlite"]; + URLByAppendingPathExtension:@"sqlite"]; _localStoreURL = localStoreURL; _containerIdentifier = containerIdentifier; _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; @@ -135,6 +134,11 @@ - (NSURL *)URLForCloudContent { return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:uuid isDirectory:YES]; } +- (NSURL *)URLForLocalStoreDirectory { + + return [self.localStoreURL URLByDeletingLastPathComponent]; +} + - (NSURL *)URLForLocalStore { return self.localStoreURL; @@ -209,6 +213,11 @@ - (void)loadStore { nil]; [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + // Make sure local store directory exists. + if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForLocalStoreDirectory].path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + // Add local store to PSC. [self.persistentStoreCoordinator lock]; [self clearStore]; @@ -245,18 +254,6 @@ - (void)loadStore { return; } - // If a migration is requested but no local store is present, don't migrate. - if (self.migrateLocalToCloud) { - if (![[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) { - [self log:@"Cannot migrate local store to cloud: local store does not exist."]; - self.migrateLocalToCloud = NO; - } - else if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) { - [self log:@"Cannot migrate local store to cloud: cloud store already exists."]; - self.migrateLocalToCloud = NO; - } - } - // Create the path to the cloud store. NSError *error = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path @@ -280,7 +277,8 @@ - (void)loadStore { [self.persistentStoreCoordinator lock]; [self clearStore]; - if (self.migrateLocalToCloud) { + // Determine whether we can seed the cloud store from the local store. + if ([self cloudSafeForSeeding] && [[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) { // First add the local store, then migrate it to the cloud store. [self log:@"Migrating local store to new cloud store."]; @@ -300,7 +298,7 @@ - (void)loadStore { error:&error]) [self error:error cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; } - // Not migrating, just add the cloud store. + // Not seeding, just add the cloud store. else if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:cloudStoreURL options:cloudStoreOptions @@ -324,6 +322,19 @@ - (void)loadStore { }]; } +- (BOOL)cloudSafeForSeeding { + + if (!self.tentativeStoreUUID) + // Migration is only safe when the storeUUID is tentative. + return NO; + + if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) + // Migration is only safe when the cloud store does not yet exist. + return NO; + + return YES; +} + - (void)observeStore { if (self.cloudEnabled) { From 261344e1faffa59b570343ca11be5dda412242e2 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 10 Feb 2013 15:27:11 -0500 Subject: [PATCH 10/35] Store URL observation fix + improved logging. [FIXED] Use a separate queue for observing changes to the store URL. [IMPROVED] Better logging in case no store was successfully added. [FIXED] Don't confirm tentative UUID when store was not successfully added. --- iCloudStoreManager/UbiquityStoreManager.m | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 59c8f9c..4fe9d8e 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -39,7 +39,9 @@ @interface UbiquityStoreManager () @end -@implementation UbiquityStoreManager +@implementation UbiquityStoreManager { + NSOperationQueue *_presentedItemOperationQueue; +} @synthesize persistentStoreCoordinator = _persistentStoreCoordinator; - (id)init { @@ -66,7 +68,9 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb // Private vars _persistentStorageQueue = [NSOperationQueue new]; - [_persistentStorageQueue setName:NSStringFromClass([self class])]; + _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass([self class])]; + _presentedItemOperationQueue = [NSOperationQueue new]; + _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass([self class])]; return self; } @@ -233,7 +237,7 @@ - (void)loadStore { self.loadingStore = NO; } - [self log:@"iCloud disabled. Loaded local store."]; + [self log:@"iCloud disabled. %@", [self.persistentStoreCoordinator.persistentStores count]? @"Loaded local store.": @"Failed to load local store."]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) @@ -305,15 +309,17 @@ - (void)loadStore { error:&error]) [self error:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; - [self confirmTentativeStoreUUID]; - [self observeStore]; + if ([self.persistentStoreCoordinator.persistentStores count]) { + [self confirmTentativeStoreUUID]; + [self observeStore]; + } } @finally { [self.persistentStoreCoordinator unlock]; self.loadingStore = NO; } - [self log:@"iCloud enabled. Loaded cloud store."]; + [self log:@"iCloud enabled. %@", [self.persistentStoreCoordinator.persistentStores count]? @"Loaded cloud store.": @"Failed to load cloud store."]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) @@ -507,9 +513,9 @@ - (NSURL *)presentedItemURL { return [self URLForLocalStore]; } -- (NSOperationQueue *)presentedItemOperationQueue { - - return self.persistentStorageQueue; +-(NSOperationQueue *)presentedItemOperationQueue { + + return _presentedItemOperationQueue; } - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { From 32503c0005cd8b3b29825891fced109ff73839d4 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 3 Mar 2013 15:10:54 -0500 Subject: [PATCH 11/35] Some cleanup + edge case / reset handling. [ADDED] Some API documentation. [UPDATED] Some cleanup. [FIXED] Migration safety check should test the right thing, not indirectly. [UPDATED] Store reset code consolidated. [FIXED] Make sure a new tentative store UUID will be used after resetting the cloud store. [FIXED] Save after cloud import to prevent losing changes. Log errors verbosely. --- iCloudStoreManager/UbiquityStoreManager.h | 10 +- iCloudStoreManager/UbiquityStoreManager.m | 140 +++++++++--------- iCloudStoreManagerExample/AppDelegate.m | 11 +- .../DetailViewController.m | 5 +- .../MasterViewController.m | 49 +++--- .../iCloudStoreManagerExample-Info.plist | 2 +- 6 files changed, 99 insertions(+), 118 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 0332f6a..b672619 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -31,19 +31,27 @@ typedef enum { UbiquityStoreManagerErrorCauseOpenLocalStore, // Error occurred while opening the local store file. UbiquityStoreManagerErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. + UbiquityStoreManagerErrorCauseImportChanges // Error occurred while importing changes from the cloud into the application's context. } UbiquityStoreManagerErrorCause; @class UbiquityStoreManager; @protocol UbiquityStoreManagerDelegate +/** The application should provide a managed object context to use for importing cloud changes. + * + * After importing the changes to the context, the context will be saved. + */ @required -- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm; +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)usm; @optional +/** Triggered when the store manager loads a persistence store. Mainly useful to be informed of whether or not cloud is enabled. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled; +/** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; +/** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; @end diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 4fe9d8e..4613d75 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -119,9 +119,7 @@ - (NSURL *)URLForCloudStoreDirectory { - (NSURL *)URLForCloudStore { // Our cloud store is in the cloud store databases directory and is identified by the active storeUUID. - NSString *uuid = self.storeUUID; - NSAssert(uuid, @"No storeUUID set."); - return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:uuid isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; + return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; } - (NSURL *)URLForCloudContentDirectory { @@ -133,9 +131,7 @@ - (NSURL *)URLForCloudContentDirectory { - (NSURL *)URLForCloudContent { // Our cloud store's logs are in the cloud store transaction logs directory and is identified by the active storeUUID. - NSString *uuid = self.storeUUID; - NSAssert(uuid, @"No storeUUID set."); - return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:uuid isDirectory:YES]; + return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:YES]; } - (NSURL *)URLForLocalStoreDirectory { @@ -315,6 +311,7 @@ - (void)loadStore { } } @finally { + [self resetTentativeStoreUUID]; [self.persistentStoreCoordinator unlock]; self.loadingStore = NO; } @@ -330,8 +327,8 @@ - (void)loadStore { - (BOOL)cloudSafeForSeeding { - if (!self.tentativeStoreUUID) - // Migration is only safe when the storeUUID is tentative. + if ([[NSUbiquitousKeyValueStore defaultStore] objectForKey:StoreUUIDKey]) + // Migration is only safe when there is no storeUUID yet (the store is not in the cloud yet). return NO; if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) @@ -367,75 +364,49 @@ - (void)observeStore { #endif } -- (void)nukeCloudContainer { +- (void)nuke:(NSURL *)directoryURL { - NSURL *cloudContainerURL = [self URLForCloudContainer]; - if (cloudContainerURL && [[NSFileManager defaultManager] fileExistsAtPath:cloudContainerURL.path]) { - [self.persistentStoreCoordinator lock]; - [self clearStore]; - - // Delete the contents of the cloud container. - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - for (NSString *subPath in [[NSFileManager defaultManager] subpathsAtPath:cloudContainerURL.path]) { - NSError *error = nil; - [coordinator coordinateWritingItemAtURL:[NSURL fileURLWithPath:subPath] options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor: - ^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:subPath]; - } - - // Unset the storeUUID so a new one will be created. - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - - [self.persistentStoreCoordinator unlock]; - [self loadStore]; - } + NSError *error = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL + options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + }]; + if (error) + [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:directoryURL.path]; } -- (void)deleteLocalStore { +- (void)nukeCloudStores { + [self.persistentStoreCoordinator lock]; [self clearStore]; - NSError *error = nil; - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - [coordinator coordinateWritingItemAtURL:[self URLForLocalStore] options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor:^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:[self URLForLocalStore].path]; + // Clean up any cloud stores and transaction logs. + [self nuke:[self URLForCloudStoreDirectory]]; + [self nuke:[self URLForCloudContentDirectory]]; + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + + [self.persistentStoreCoordinator unlock]; [self loadStore]; } -- (void)deleteCloudStore { +- (void)nukeLocalStores { + [self.persistentStoreCoordinator lock]; [self clearStore]; - NSError *error = nil; - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - - NSURL *cloudStoreURL = [self URLForCloudStore]; - [coordinator coordinateWritingItemAtURL:cloudStoreURL options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor:^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; + // Remove just the local store. + [self nuke:[self URLForLocalStoreDirectory]]; + + [self.persistentStoreCoordinator unlock]; [self loadStore]; } @@ -494,15 +465,24 @@ - (NSString *)storeUUID { * When a tentativeStoreUUID is set, this operation confirms it and writes it as the new storeUUID to the iCloud KVS. */ - (void)confirmTentativeStoreUUID { - + if (self.tentativeStoreUUID) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; [cloud synchronize]; - self.tentativeStoreUUID = nil; + + [self resetTentativeStoreUUID]; } } +/** + * When a tentativeStoreUUID is set, this operation resets it so that a new one will be generated if necessary. + */ +- (void)resetTentativeStoreUUID { + + self.tentativeStoreUUID = nil; +} + #pragma mark - NSFilePresenter - (NSURL *)presentedItemURL { @@ -530,14 +510,12 @@ - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError - (void)applicationDidBecomeActive:(NSNotification *)note { - // Check for account changes. + // Check for iCloud account changes. NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; id lastIdentityToken = [local objectForKey:CloudIdentityKey]; id currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; - if (![lastIdentityToken isEqual:currentIdentityToken]) { + if (![lastIdentityToken isEqual:currentIdentityToken]) [self cloudStoreChanged:nil]; - return; - } } - (void)keyValueStoreChanged:(NSNotification *)note { @@ -546,6 +524,12 @@ - (void)keyValueStoreChanged:(NSNotification *)note { [self cloudStoreChanged:nil]; } +/** + * Triggered when: + * 1. Ubiquity identity changed (eg. iCloud account changed in settings) + * 2. Store file was deleted (eg. iCloud container deleted in settings) + * 3. StoreUUID changed (eg. switched to a new cloud store on another device) + */ - (void)cloudStoreChanged:(NSNotification *)note { // Update the identity token in case it changed. @@ -554,6 +538,10 @@ - (void)cloudStoreChanged:(NSNotification *)note { [local setObject:identityToken forKey:CloudIdentityKey]; [local synchronize]; + // Don't reload the store when the local one is active. + if (!self.cloudEnabled) + return; + // Reload the store. [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, identityToken]; [self loadStore]; @@ -563,9 +551,19 @@ - (void)mergeChanges:(NSNotification *)note { [self log:@"Cloud store updates:\n%@", note.userInfo]; [self.persistentStorageQueue addOperationWithBlock:^{ - NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; + NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; [moc performBlockAndWait:^{ [moc mergeChangesFromContextDidSaveNotification:note]; + + NSError *error = nil; + if (![moc save:&error]) { + [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; + + NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; + if ([detailedErrors count]) + for (NSError *detailedError in detailedErrors) + [self error:detailedError cause:UbiquityStoreManagerErrorCauseImportChanges context:nil]; + } }]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index 2cefd8d..7ec8092 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -36,16 +36,11 @@ + (AppDelegate *)appDelegate { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // STEP 1 - Initialize the UbiquityStoreManager - ubiquityStoreManager = [[UbiquityStoreManager alloc] initWithManagedObjectModel: [self managedObjectModel] - localStoreURL: [self storeURL] - containerIdentifier: nil - additionalStoreOptions: nil]; + ubiquityStoreManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:[self managedObjectModel] + localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil]; // STEP 2a - Setup the delegate ubiquityStoreManager.delegate = self; - // For test purposes only. NOT FOR USE IN PRODUCTION - ubiquityStoreManager.hardResetEnabled = YES; - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { @@ -86,8 +81,6 @@ - (void)applicationDidEnterBackground:(UIApplication *)application - (void)applicationWillEnterForeground:(UIApplication *)application { - // STEP 2b - Check to make sure user has not deleted the iCloud data from Settings - [self.ubiquityStoreManager checkiCloudStatus]; } - (void)applicationDidBecomeActive:(UIApplication *)application diff --git a/iCloudStoreManagerExample/DetailViewController.m b/iCloudStoreManagerExample/DetailViewController.m index 8c095a4..9962332 100644 --- a/iCloudStoreManagerExample/DetailViewController.m +++ b/iCloudStoreManagerExample/DetailViewController.m @@ -86,10 +86,7 @@ - (void)splitViewController:(UISplitViewController *)splitController willShowVie #pragma mark - Table View - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return 1; - else - return 0; + return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { diff --git a/iCloudStoreManagerExample/MasterViewController.m b/iCloudStoreManagerExample/MasterViewController.m index d9421e5..f0274a0 100644 --- a/iCloudStoreManagerExample/MasterViewController.m +++ b/iCloudStoreManagerExample/MasterViewController.m @@ -29,14 +29,13 @@ - (IBAction)setiCloudState:(id)sender { UISwitch *aSwitch = sender; // STEP 5a - Set the state of the UbiquityStoreManager to reflect the current UI - [[[AppDelegate appDelegate] ubiquityStoreManager] useiCloudStore:aSwitch.on alertUser:YES]; + [[[AppDelegate appDelegate] ubiquityStoreManager] setCloudEnabled:aSwitch.on]; } - (IBAction)cleariCloud:(id)sender { - iCloudSwitch.on = NO; // STEP 6 - UbiquityStoreManager hard reset. FOR TESTING ONLY! Do not expose to the end user! - [[[AppDelegate appDelegate] ubiquityStoreManager] hardResetCloudStorage]; + [[[AppDelegate appDelegate] ubiquityStoreManager] deleteCloudStore]; } - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil @@ -54,22 +53,17 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil - (void)reloadFetchedResults:(NSNotification*)note { - // STEP 7a - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) { - - // Refetch the data - self.fetchedResultsController = nil; - [self fetchedResultsController]; - - if (note) { - [self.tableView reloadData]; - - // STEP 5b - Display current state of the UbiquityStoreManager - BOOL enabled = [[AppDelegate appDelegate] ubiquityStoreManager].iCloudEnabled; - [iCloudSwitch setOn:enabled animated:YES]; - } - } + // Refetch the data + self.fetchedResultsController = nil; + [self fetchedResultsController]; + + if (note) { + [self.tableView reloadData]; + + // STEP 5b - Display current state of the UbiquityStoreManager + BOOL enabled = [[AppDelegate appDelegate] ubiquityStoreManager].cloudEnabled; + [iCloudSwitch setOn:enabled animated:YES]; + } } - (void)viewDidLoad { @@ -87,13 +81,13 @@ - (void)viewDidLoad { // Observe the app delegate telling us when it's finished asynchronously setting up the persistent store [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(reloadFetchedResults:) - name: RefetchAllDatabaseDataNotificationKey + name: UbiquityManagedStoreDidChangeNotification object: [[AppDelegate appDelegate] ubiquityStoreManager]]; self.tableView.tableHeaderView = self.tableHeaderView; // STEP 5c - Display current state of the UbiquityStoreManager - self.iCloudSwitch.on = [[AppDelegate appDelegate] ubiquityStoreManager].iCloudEnabled; + self.iCloudSwitch.on = [[AppDelegate appDelegate] ubiquityStoreManager].cloudEnabled; } - (void)viewDidUnload @@ -141,11 +135,7 @@ - (void)insertNewObject:(id)sender - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // STEP 7b - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if ([[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return [[self.fetchedResultsController sections] count]; - else - return 0; + return [[self.fetchedResultsController sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section @@ -216,7 +206,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath } else { self.detailViewController.detailItem = object; } - self.detailViewController.fileList = [[[AppDelegate appDelegate] ubiquityStoreManager] fileList]; + self.detailViewController.fileList = [[NSFileManager defaultManager] subpathsAtPath:[[[AppDelegate appDelegate] ubiquityStoreManager].URLForCloudContainer path]]; [self.detailViewController.tableView reloadData]; } @@ -224,11 +214,6 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath - (NSFetchedResultsController *)fetchedResultsController { - // STEP 7c - Do not allow use of any NSManagedObjectContext until UbiquityStoreManager is ready - - if (![[AppDelegate appDelegate] ubiquityStoreManager].isReady) - return nil; - if (__fetchedResultsController != nil) { return __fetchedResultsController; } diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist b/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist index 2900d33..9b2b00a 100644 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist +++ b/iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.yodelcode.iCloudManager + com.lyndir.lhunath.iCloudManager CFBundleInfoDictionaryVersion 6.0 CFBundleName From d80569b200254f3deceef73db302faa981f1d61d Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 3 Mar 2013 15:18:03 -0500 Subject: [PATCH 12/35] Some cleanup + edge case / reset handling. [ADDED] Some API documentation. [UPDATED] Some cleanup. [FIXED] Migration safety check should test the right thing, not indirectly. [UPDATED] Store reset code consolidated. [FIXED] Make sure a new tentative store UUID will be used after resetting the cloud store. [FIXED] Save after cloud import to prevent losing changes. Log errors verbosely. --- iCloudStoreManager/UbiquityStoreManager.h | 10 +- iCloudStoreManager/UbiquityStoreManager.m | 140 +++++++++++----------- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 0332f6a..b672619 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -31,19 +31,27 @@ typedef enum { UbiquityStoreManagerErrorCauseOpenLocalStore, // Error occurred while opening the local store file. UbiquityStoreManagerErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. + UbiquityStoreManagerErrorCauseImportChanges // Error occurred while importing changes from the cloud into the application's context. } UbiquityStoreManagerErrorCause; @class UbiquityStoreManager; @protocol UbiquityStoreManagerDelegate +/** The application should provide a managed object context to use for importing cloud changes. + * + * After importing the changes to the context, the context will be saved. + */ @required -- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm; +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)usm; @optional +/** Triggered when the store manager loads a persistence store. Mainly useful to be informed of whether or not cloud is enabled. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled; +/** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; +/** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; @end diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 4fe9d8e..4613d75 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -119,9 +119,7 @@ - (NSURL *)URLForCloudStoreDirectory { - (NSURL *)URLForCloudStore { // Our cloud store is in the cloud store databases directory and is identified by the active storeUUID. - NSString *uuid = self.storeUUID; - NSAssert(uuid, @"No storeUUID set."); - return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:uuid isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; + return [[[self URLForCloudStoreDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:NO] URLByAppendingPathExtension:@"sqlite"]; } - (NSURL *)URLForCloudContentDirectory { @@ -133,9 +131,7 @@ - (NSURL *)URLForCloudContentDirectory { - (NSURL *)URLForCloudContent { // Our cloud store's logs are in the cloud store transaction logs directory and is identified by the active storeUUID. - NSString *uuid = self.storeUUID; - NSAssert(uuid, @"No storeUUID set."); - return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:uuid isDirectory:YES]; + return [[self URLForCloudContentDirectory] URLByAppendingPathComponent:self.storeUUID isDirectory:YES]; } - (NSURL *)URLForLocalStoreDirectory { @@ -315,6 +311,7 @@ - (void)loadStore { } } @finally { + [self resetTentativeStoreUUID]; [self.persistentStoreCoordinator unlock]; self.loadingStore = NO; } @@ -330,8 +327,8 @@ - (void)loadStore { - (BOOL)cloudSafeForSeeding { - if (!self.tentativeStoreUUID) - // Migration is only safe when the storeUUID is tentative. + if ([[NSUbiquitousKeyValueStore defaultStore] objectForKey:StoreUUIDKey]) + // Migration is only safe when there is no storeUUID yet (the store is not in the cloud yet). return NO; if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) @@ -367,75 +364,49 @@ - (void)observeStore { #endif } -- (void)nukeCloudContainer { +- (void)nuke:(NSURL *)directoryURL { - NSURL *cloudContainerURL = [self URLForCloudContainer]; - if (cloudContainerURL && [[NSFileManager defaultManager] fileExistsAtPath:cloudContainerURL.path]) { - [self.persistentStoreCoordinator lock]; - [self clearStore]; - - // Delete the contents of the cloud container. - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - for (NSString *subPath in [[NSFileManager defaultManager] subpathsAtPath:cloudContainerURL.path]) { - NSError *error = nil; - [coordinator coordinateWritingItemAtURL:[NSURL fileURLWithPath:subPath] options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor: - ^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:subPath]; - } - - // Unset the storeUUID so a new one will be created. - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - - [self.persistentStoreCoordinator unlock]; - [self loadStore]; - } + NSError *error = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL + options:NSFileCoordinatorWritingForDeleting + error:&error byAccessor: + ^(NSURL *newURL) { + NSError *error_ = nil; + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + }]; + if (error) + [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:directoryURL.path]; } -- (void)deleteLocalStore { +- (void)nukeCloudStores { + [self.persistentStoreCoordinator lock]; [self clearStore]; - NSError *error = nil; - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - [coordinator coordinateWritingItemAtURL:[self URLForLocalStore] options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor:^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:[self URLForLocalStore].path]; + // Clean up any cloud stores and transaction logs. + [self nuke:[self URLForCloudStoreDirectory]]; + [self nuke:[self URLForCloudContentDirectory]]; + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + + [self.persistentStoreCoordinator unlock]; [self loadStore]; } -- (void)deleteCloudStore { +- (void)nukeLocalStores { + [self.persistentStoreCoordinator lock]; [self clearStore]; - NSError *error = nil; - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; - - NSURL *cloudStoreURL = [self URLForCloudStore]; - [coordinator coordinateWritingItemAtURL:cloudStoreURL options:NSFileCoordinatorWritingForDeleting - error:&error byAccessor:^(NSURL *newURL) { - NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; - }]; - - if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:cloudStoreURL.path]; + // Remove just the local store. + [self nuke:[self URLForLocalStoreDirectory]]; + + [self.persistentStoreCoordinator unlock]; [self loadStore]; } @@ -494,15 +465,24 @@ - (NSString *)storeUUID { * When a tentativeStoreUUID is set, this operation confirms it and writes it as the new storeUUID to the iCloud KVS. */ - (void)confirmTentativeStoreUUID { - + if (self.tentativeStoreUUID) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; [cloud synchronize]; - self.tentativeStoreUUID = nil; + + [self resetTentativeStoreUUID]; } } +/** + * When a tentativeStoreUUID is set, this operation resets it so that a new one will be generated if necessary. + */ +- (void)resetTentativeStoreUUID { + + self.tentativeStoreUUID = nil; +} + #pragma mark - NSFilePresenter - (NSURL *)presentedItemURL { @@ -530,14 +510,12 @@ - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError - (void)applicationDidBecomeActive:(NSNotification *)note { - // Check for account changes. + // Check for iCloud account changes. NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; id lastIdentityToken = [local objectForKey:CloudIdentityKey]; id currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; - if (![lastIdentityToken isEqual:currentIdentityToken]) { + if (![lastIdentityToken isEqual:currentIdentityToken]) [self cloudStoreChanged:nil]; - return; - } } - (void)keyValueStoreChanged:(NSNotification *)note { @@ -546,6 +524,12 @@ - (void)keyValueStoreChanged:(NSNotification *)note { [self cloudStoreChanged:nil]; } +/** + * Triggered when: + * 1. Ubiquity identity changed (eg. iCloud account changed in settings) + * 2. Store file was deleted (eg. iCloud container deleted in settings) + * 3. StoreUUID changed (eg. switched to a new cloud store on another device) + */ - (void)cloudStoreChanged:(NSNotification *)note { // Update the identity token in case it changed. @@ -554,6 +538,10 @@ - (void)cloudStoreChanged:(NSNotification *)note { [local setObject:identityToken forKey:CloudIdentityKey]; [local synchronize]; + // Don't reload the store when the local one is active. + if (!self.cloudEnabled) + return; + // Reload the store. [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, identityToken]; [self loadStore]; @@ -563,9 +551,19 @@ - (void)mergeChanges:(NSNotification *)note { [self log:@"Cloud store updates:\n%@", note.userInfo]; [self.persistentStorageQueue addOperationWithBlock:^{ - NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityStoreManager:self]; + NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; [moc performBlockAndWait:^{ [moc mergeChangesFromContextDidSaveNotification:note]; + + NSError *error = nil; + if (![moc save:&error]) { + [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; + + NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; + if ([detailedErrors count]) + for (NSError *detailedError in detailedErrors) + [self error:detailedError cause:UbiquityStoreManagerErrorCauseImportChanges context:nil]; + } }]; dispatch_async(dispatch_get_main_queue(), ^{ From ff9e2d4e4daf61668b8859901b62ee05325cbbf8 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 02:34:29 -0500 Subject: [PATCH 13/35] Migration strategies + more robust API + more robust store loading. [ADDED] Migration strategies: - iOS migration (as before) just uses the PSC's migratePersistentStore. - CopyEntities migration attempts to copy all the entities from the source store to the destination and rebuild relationships. (WIP) - Manual migration allows the application to do its own migration. - None migration does no migration and just loads the existing store or creates an empty one if one doesn't exist. This was needed mostly because the iOS migration used before is bugged in iOS 6.1. [REMOVED] Direct access to the PSC. The idea is now that you init your MOC whenever the delegate is triggered for a successful store load. This means much more reliable and consistent access to the store coordinator and only when the store is ready. [ADDED] Delegate method to learn when persistence becomes unavailable (because it's loading a new store). [ADDED] Delegate method for dealing with failure to load stores. [ADDED] Now the delegate must be set at init time to reliably receive the store load notification that's needed to init your MOC. [IMPROVED] The PSC is now locked whenever it is unavailable (store loading) to prevent the application from using it. [IMPROVED] Much more robust store loading code. [IMPROVED] Better logging and more information in delegate methods. [ADDED] Missing nukeCloudContainer which deletes the entire application container from iCloud. --- iCloudStoreManager/UbiquityStoreManager.h | 60 ++- iCloudStoreManager/UbiquityStoreManager.m | 536 ++++++++++++++++------ 2 files changed, 428 insertions(+), 168 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index b672619..8ee132a 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -25,6 +25,7 @@ extern NSString *const UbiquityManagedStoreDidChangeNotification; extern NSString *const UbiquityManagedStoreDidImportChangesNotification; typedef enum { + UbiquityStoreManagerErrorCauseNoAccount, // The user is not logged into iCloud on this device. UbiquityStoreManagerErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. UbiquityStoreManagerErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. UbiquityStoreManagerErrorCauseClearStore, // Error occurred while removing the active store from the coordinator. @@ -34,6 +35,13 @@ typedef enum { UbiquityStoreManagerErrorCauseImportChanges // Error occurred while importing changes from the cloud into the application's context. } UbiquityStoreManagerErrorCause; +typedef enum { + UbiquityStoreManagerMigrationStrategyCopyEntities, // Migrate by copying all entities from the active store to the new store. + UbiquityStoreManagerMigrationStrategyIOS, // Migrate using iOS' migration routines (bugged for: cloud -> local on iOS 6.0, local -> cloud on iOS 6.1). + UbiquityStoreManagerMigrationStrategyManual, // Migrate using the delegate's -ubiquityStoreManager:manuallyMigrateStore:toStore:. + UbiquityStoreManagerMigrationStrategyNone, // Don't migrate, just create an empty destination store. +} UbiquityStoreManagerMigrationStrategy; + @class UbiquityStoreManager; @protocol UbiquityStoreManagerDelegate @@ -43,16 +51,40 @@ typedef enum { * After importing the changes to the context, the context will be saved. */ @required -- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)usm; +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager; +/** Triggered when the store manager loads a persistence store. + * + * This is where you'll init/update your application's persistence layer. + */ +@required +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; + +/** Triggered when the store manager begins loading a persistence store. + * + * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:, the application should not be using the persistence coordinator. Ideally, you could unset your managed object contexts here. + * Also useful for indicating in your user interface that the store is loading. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; +/** Triggered when the store manager fails to loads a persistence store. Useful to decide what to do to make a store available to the application. If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. */ @optional -/** Triggered when the store manager loads a persistence store. Mainly useful to be informed of whether or not cloud is enabled. */ -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToCloud:(BOOL)cloudEnabled; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore; /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ +@optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; /** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ +@optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; +/** Triggered when the store manager needs to perform a manual store migration. + * @param error Write out an error object here when the migration fails. + * @return YES when the migration was successful and the new store may be loaded. NO to error out and not load the new store (new store will be cleaned up if it exists). + */ +@optional +- (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager + manuallyMigrateStore:(NSURL *)oldStore withOptions:oldStoreOptions + toStore:(NSURL *)newStore withOptions:newStoreOptions error:(NSError **)error; @end @@ -61,42 +93,38 @@ typedef enum { // The delegate provides the managed object context to use and is informed of events in the ubiquity manager. @property (nonatomic, weak) id delegate; +// Determines what strategy to use when migrating from one store to another (eg. local -> cloud). +@property (nonatomic, assign) UbiquityStoreManagerMigrationStrategy migrationStrategy; + // Indicates whether the iCloud store or the local store is in use. @property (nonatomic) BOOL cloudEnabled; -// The coordinator provides access to this manager's active store. -@property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; - -/** - * Start managing an optionally ubiquitous store coordinator. Default settings will be used. - */ -- (id)init; - /** Start managing an optionally ubiquitous store coordinator. * @param contentName The name of the local and cloud stores that this manager will create. If nil, "UbiquityStore" will be used. * @param model The managed object model the store should use. If nil, all the main bundle's models will be merged. * @param localStoreURL The location where the non-ubiquitous (local) store should be kept. If nil, the local store will be put in the application support directory. * @param containerIdentifier The identifier of the ubiquity container to use for the ubiquitous store. If nil, the entitlement's primary container identifier will be used. * @param additionalStoreOptions Additional persistence options that the stores should be initialized with. + * @param delegate The application controller that will be handling the application's persistence responsibilities. */ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions; + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions delegate:(id)delegate; /** - * This will delete the local iCloud data for this application. There is no recovery. A new iCloud store will be initialized if enabled. + * This will delete all the data from iCloud for this application. There is no recovery. A new iCloud store will be created if enabled. */ -- (void)nukeCloudContainer; +- (BOOL)nukeCloudContainer; /** * This will delete the local store. There is no recovery. */ -- (void)deleteLocalStore; +- (BOOL)deleteLocalStore; /** * This will delete the iCloud store. Theoretically, it should be rebuilt from the iCloud transaction logs. * TODO: Verify claim. */ -- (void)deleteCloudStore; +- (BOOL)deleteCloudStore; /** * Determine whether it's safe to seed the cloud store with a local store. diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 4613d75..3b93f06 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -34,7 +34,7 @@ @interface UbiquityStoreManager () @property (nonatomic, readonly) NSString *storeUUID; @property (nonatomic, strong) NSString *tentativeStoreUUID; @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; -@property (nonatomic) BOOL loadingStore; +@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; @end @@ -42,20 +42,16 @@ @interface UbiquityStoreManager () @implementation UbiquityStoreManager { NSOperationQueue *_presentedItemOperationQueue; } -@synthesize persistentStoreCoordinator = _persistentStoreCoordinator; - -- (id)init { - - return self = [self initStoreNamed:nil withManagedObjectModel:nil localStoreURL:nil containerIdentifier:nil additionalStoreOptions:nil]; -} - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL - containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions { + containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions + delegate:(id )delegate { if (!(self = [super init])) return nil; // Parameters + _delegate = delegate; _contentName = contentName == nil? @"UbiquityStore": contentName; _model = model == nil? [NSManagedObjectModel mergedModelFromBundles:nil]: model; if (!localStoreURL) @@ -69,15 +65,20 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb // Private vars _persistentStorageQueue = [NSOperationQueue new]; _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass([self class])]; + _persistentStorageQueue.maxConcurrentOperationCount = 1; _presentedItemOperationQueue = [NSOperationQueue new]; _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass([self class])]; + [self loadStore]; + return self; } - (void)dealloc { + [self.persistentStoreCoordinator tryLock]; [self clearStore]; + [self.persistentStoreCoordinator unlock]; } #pragma mark - File Handling @@ -177,154 +178,364 @@ - (void)clearStore { [[NSNotificationCenter defaultCenter] removeObserver:self]; // Remove the store from the coordinator. - NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; - BOOL pscLockedByUs = [psc tryLock]; NSError *error = nil; - BOOL failed = NO; - - for (NSPersistentStore *store in psc.persistentStores) - if (![psc removePersistentStore:store error:&error]) { - failed = YES; + for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) + if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) [self error:error cause:UbiquityStoreManagerErrorCauseClearStore context:store]; - } - if (pscLockedByUs) - [psc unlock]; - if (failed) - // Try to recover by throwing out the PSC. - _persistentStoreCoordinator = nil; + if ([self.persistentStoreCoordinator.persistentStores count]) { + // We couldn't remove all the stores, make a new PSC instead. + [self.persistentStoreCoordinator unlock]; + self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + [self.persistentStoreCoordinator lock]; + } } - (void)loadStore { - @synchronized (self) { - if (self.loadingStore) - return; - self.loadingStore = YES; - } - - if (!self.cloudEnabled) { - @try { - // Load local store if iCloud is disabled. - NSError *error = nil; - NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - @YES, NSMigratePersistentStoresAutomaticallyOption, - @YES, NSInferMappingModelAutomaticallyOption, - nil]; - [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - - // Make sure local store directory exists. - if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForLocalStoreDirectory].path - withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) + [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; - // Add local store to PSC. - [self.persistentStoreCoordinator lock]; - [self clearStore]; - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:[self URLForLocalStore] - options:localStoreOptions - error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; - [self observeStore]; - } - @finally { - [self.persistentStoreCoordinator unlock]; - self.loadingStore = NO; - } + if (!self.persistentStoreCoordinator) + self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - [self log:@"iCloud disabled. %@", [self.persistentStoreCoordinator.persistentStores count]? @"Loaded local store.": @"Failed to load local store."]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) - [self.delegate ubiquityStoreManager:self didSwitchToCloud:NO]; - }); + if (self.cloudEnabled) + [self loadCloudStore]; + else + [self loadLocalStore]; +} - return; - } +- (void)loadCloudStore { - // Otherwise, load iCloud store asynchronously (init of iCloud may take some time). + // Load iCloud store asynchronously (init of iCloud may take some time). [self.persistentStorageQueue addOperationWithBlock:^{ + if (![self.persistentStoreCoordinator tryLock]) + // PSC is locked and busy with another operation. We can't use it. + return; + + NSError *error = nil; + UbiquityStoreManagerErrorCause cause; @try { + [self clearStore]; + + // Check if the user is logged into iCloud on the device. if (![self URLForCloudContainer]) { - // iCloud is not enabled on this device. Disable iCloud in the app (will cause a re-load using the local store). - // TODO: Notify user? - self.loadingStore = NO; - self.cloudEnabled = NO; + cause = UbiquityStoreManagerErrorCauseNoAccount; return; } // Create the path to the cloud store. - NSError *error = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; // Add cloud store to PSC. - NSURL *cloudStoreURL = [self URLForCloudStore]; - NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - self.contentName, NSPersistentStoreUbiquitousContentNameKey, - [self URLForCloudContent], NSPersistentStoreUbiquitousContentURLKey, - @YES, NSMigratePersistentStoresAutomaticallyOption, - @YES, NSInferMappingModelAutomaticallyOption, - nil]; - NSMutableDictionary *migratingStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - @YES, NSReadOnlyPersistentStoreOption, - nil]; + NSURL *cloudStoreURL = [self URLForCloudStore]; + NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + self.contentName, NSPersistentStoreUbiquitousContentNameKey, + [self URLForCloudContent], NSPersistentStoreUbiquitousContentURLKey, + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSReadOnlyPersistentStoreOption, + nil]; [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - [migratingStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - - [self.persistentStoreCoordinator lock]; - [self clearStore]; - - // Determine whether we can seed the cloud store from the local store. - if ([self cloudSafeForSeeding] && [[NSFileManager defaultManager] fileExistsAtPath:[self URLForLocalStore].path]) { - // First add the local store, then migrate it to the cloud store. - [self log:@"Migrating local store to new cloud store."]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - // Add the store to migrate - NSPersistentStore *migratingStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:[self URLForLocalStore] - options:migratingStoreOptions + // Now load the cloud store. If possible, first migrate the local store to it. + NSURL *localStoreURL = [self URLForLocalStore]; + UbiquityStoreManagerMigrationStrategy migrationStrategy = self.migrationStrategy; + if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) + migrationStrategy = UbiquityStoreManagerMigrationStrategyNone; + + switch (migrationStrategy) { + case UbiquityStoreManagerMigrationStrategyCopyEntities: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyCopyEntities"]; + + // Open local and cloud store. + NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + NSPersistentStore *localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:localStoreURL + options:localStoreOptions + error:&error]; + if (!localStore) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL.path]; + break; + } + + NSPersistentStoreCoordinator *cloudCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + NSPersistentStore *cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]; + if (!cloudStore) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + break; + } + + // Set up contexts for them. + NSManagedObjectContext *localContext = [NSManagedObjectContext new]; + NSManagedObjectContext *cloudContext = [NSManagedObjectContext new]; + localContext.persistentStoreCoordinator = localCoordinator; + cloudContext.persistentStoreCoordinator = cloudCoordinator; + + // Copy metadata. + NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:localStore] mutableCopy]; + [metadata addEntriesFromDictionary:[cloudCoordinator metadataForPersistentStore:cloudStore]]; + [cloudCoordinator setMetadata:metadata forPersistentStore:cloudStore]; + + // Migrate entities. + BOOL migrationFailure = NO; + NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; + for (NSEntityDescription *entity in self.model.entities) { + NSFetchRequest *fetch = [NSFetchRequest new]; + fetch.entity = entity; + fetch.fetchBatchSize = 500; + fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; + + NSArray *localObjects = [localContext executeFetchRequest:fetch error:&error]; + if (!localObjects) { + migrationFailure = YES; + break; + } + + for (NSManagedObject *localObject in localObjects) + [self copyMigrateObject:localObject toContext:cloudContext usingMigrationCache:migratedIDsBySourceID]; + } + + // Handle failure by cleaning up the cloud store. + if (migrationFailure) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + + if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) + [self error:error cause:cause = UbiquityStoreManagerErrorCauseClearStore context:cloudStoreURL.path]; + [self removeItemAtURL:cloudStoreURL]; + break; + } + + // Add the store now that migration is finished. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + } + + break; + } + + case UbiquityStoreManagerMigrationStrategyIOS: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyIOS"]; + + // Add the store to migrate. + NSPersistentStore *localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:localStoreURL + options:localStoreOptions error:&error]; - if (!migratingStore) - [self error:error cause:UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; - - else if (![self.persistentStoreCoordinator migratePersistentStore:migratingStore - toURL:cloudStoreURL - options:cloudStoreOptions - withType:NSSQLiteStoreType - error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + if (!localStore) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL]; + break; + } + + if (![self.persistentStoreCoordinator migratePersistentStore:localStore + toURL:cloudStoreURL + options:cloudStoreOptions + withType:NSSQLiteStoreType + error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + + if (![self.persistentStoreCoordinator removePersistentStore:localStore error:&error]) + [self error:error cause:cause = UbiquityStoreManagerErrorCauseClearStore context:cloudStoreURL.path]; + } + break; + } + + case UbiquityStoreManagerMigrationStrategyManual: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyManual"]; + + if (![self.delegate ubiquityStoreManager:self + manuallyMigrateStore:localStoreURL withOptions:localStoreOptions + toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + [self removeItemAtURL:cloudStoreURL]; + break; + } + + // Add the store now that migration is finished. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + } + + break; + } + + case UbiquityStoreManagerMigrationStrategyNone: { + [self log:@"Loading cloud store without local store migration."]; + + // Just add the store without first migrating to it. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + } + break; + } } - // Not seeding, just add the cloud store. - else if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; - - if ([self.persistentStoreCoordinator.persistentStores count]) { + } + @catch (id exception) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; + if (exception) + [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; + if (error) + [userInfo setObject:error forKey:NSUnderlyingErrorKey]; + [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] + cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:exception]; + + // Clean up any local stores that may still be present. + for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) + if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) + [self error:error cause:UbiquityStoreManagerErrorCauseClearStore context:store]; + } + @finally { + BOOL cloudWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; + if (cloudWasEnabled) { [self confirmTentativeStoreUUID]; + [self log:@"iCloud enabled (UUID:%@) and successfully loaded cloud store.", self.storeUUID]; [self observeStore]; } - } - @finally { - [self resetTentativeStoreUUID]; + else { + [self resetTentativeStoreUUID]; + + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { + [self log:@"iCloud enabled but failed to load cloud store (cause:%u). Application will handle failure.", cause]; + } else { + [self log:@"iCloud enabled but failed to load cloud store (cause:%u). Will fall back to local store.", cause]; + } + } [self.persistentStoreCoordinator unlock]; - self.loadingStore = NO; + + dispatch_async(dispatch_get_main_queue(), ^{ + if (cloudWasEnabled) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; + } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause wasCloud:YES]; + else + self.cloudEnabled = NO; + }); + } + }]; +} + +- (void)loadLocalStore { + + if (![self.persistentStoreCoordinator tryLock]) + // PSC is locked and busy with another operation. We can't use it. + return; + + UbiquityStoreManagerErrorCause cause; + @try { + [self clearStore]; + + // Load local store if iCloud is disabled. + NSError *error = nil; + NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + + // Make sure local store directory exists. + if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForLocalStoreDirectory].path + withIntermediateDirectories:YES attributes:nil error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + return; + } + + // Add local store to PSC. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:[self URLForLocalStore] + options:localStoreOptions + error:&error]) { + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; + return; + } + } + @finally { + BOOL localWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; + if (localWasEnabled) { + [self log:@"iCloud disabled and successfully loaded local store."]; + [self observeStore]; + } + else { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { + [self log:@"iCloud disabled but failed to load local store (cause:%u). Application will handle failure.", cause]; + } else { + [self log:@"iCloud disabled but failed to load local store (cause:%u). No store available to application.", cause]; + } + } + [self.persistentStoreCoordinator unlock]; - [self log:@"iCloud enabled. %@", [self.persistentStoreCoordinator.persistentStores count]? @"Loaded cloud store.": @"Failed to load cloud store."]; dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didSwitchToCloud:)]) - [self.delegate ubiquityStoreManager:self didSwitchToCloud:YES]; + if (localWasEnabled) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:NO]; + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; + } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause wasCloud:NO]; }); - }]; + } } +- (id)copyMigrateObject:(NSManagedObject *)sourceObject toContext:(NSManagedObjectContext *)destinationContext usingMigrationCache:(NSMutableDictionary *)migratedIDsBySourceID { + + if (!sourceObject) + return nil; + + NSManagedObjectID *destinationObjectID = [migratedIDsBySourceID objectForKey:sourceObject.objectID]; + if (destinationObjectID) + return [destinationContext objectWithID:destinationObjectID]; + + @autoreleasepool { + // Create migrated object. + NSEntityDescription *entity = sourceObject.entity; + NSManagedObject *destinationObject = [NSEntityDescription insertNewObjectForEntityForName:entity.name inManagedObjectContext:destinationContext]; + [migratedIDsBySourceID setObject:destinationObject.objectID forKey:sourceObject.objectID]; + + // Set attributes + for (NSString *key in entity.attributesByName.allKeys) + [destinationObject setPrimitiveValue:[sourceObject primitiveValueForKey:key] forKey:key]; + + // Set relationships recursively + for (NSRelationshipDescription *relationDescription in entity.relationshipsByName.allValues) { + NSString *key = relationDescription.name; + id value = nil; + + if (relationDescription.isToMany) { + value = [[destinationObject primitiveValueForKey:key] mutableCopy]; + + for (NSManagedObject *element in [sourceObject primitiveValueForKey:key]) + [value addObject:[self copyMigrateObject:element toContext:destinationContext usingMigrationCache:migratedIDsBySourceID]]; + } + else + value = [self copyMigrateObject:[sourceObject primitiveValueForKey:key] toContext:destinationContext usingMigrationCache:migratedIDsBySourceID]; + + [destinationObject setPrimitiveValue:value forKey:key]; + } + + return destinationObject; + } +} + + - (BOOL)cloudSafeForSeeding { if ([[NSUbiquitousKeyValueStore defaultStore] objectForKey:StoreUUIDKey]) @@ -364,7 +575,7 @@ - (void)observeStore { #endif } -- (void)nuke:(NSURL *)directoryURL { +- (void)removeItemAtURL:(NSURL *)directoryURL { NSError *error = nil; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL @@ -379,15 +590,19 @@ - (void)nuke:(NSURL *)directoryURL { [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:directoryURL.path]; } -- (void)nukeCloudStores { +- (BOOL)nukeCloudContainer { + + if (![self.persistentStoreCoordinator tryLock]) { + [self log:@"Cannot nuke the cloud container: Manager is locked."]; + return NO; + } + [self log:@"Will nuke the cloud container."]; - [self.persistentStoreCoordinator lock]; [self clearStore]; - - // Clean up any cloud stores and transaction logs. - [self nuke:[self URLForCloudStoreDirectory]]; - [self nuke:[self URLForCloudContentDirectory]]; + // Delete the whole cloud container. + [self removeItemAtURL:[self URLForCloudContainer]]; + // Unset the storeUUID so a new one will be created. [self resetTentativeStoreUUID]; NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; @@ -396,38 +611,57 @@ - (void)nukeCloudStores { [self.persistentStoreCoordinator unlock]; [self loadStore]; + + return YES; } -- (void)nukeLocalStores { +- (BOOL)deleteCloudStore { + + if (![self.persistentStoreCoordinator tryLock]) { + [self log:@"Cannot delete the cloud store: Manager is locked."]; + return NO; + } + [self log:@"Will delete the cloud store (UUID:%@).", self.storeUUID]; - [self.persistentStoreCoordinator lock]; [self clearStore]; - // Remove just the local store. - [self nuke:[self URLForLocalStoreDirectory]]; + // Clean up any cloud stores and transaction logs. + [self removeItemAtURL:[self URLForCloudStoreDirectory]]; + [self removeItemAtURL:[self URLForCloudContentDirectory]]; + + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; [self.persistentStoreCoordinator unlock]; [self loadStore]; + + return YES; } -#pragma mark - Properties +- (BOOL)deleteLocalStore { -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { + if (![self.persistentStoreCoordinator tryLock]) { + [self log:@"Cannot delete the local store: Manager is locked."]; + return NO; + } + [self log:@"Will delete the local store."]; - if (!_persistentStoreCoordinator) { - _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + [self clearStore]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) - name:NSPersistentStoreDidImportUbiquitousContentChangesNotification - object:_persistentStoreCoordinator]; - } + // Remove just the local store. + [self removeItemAtURL:[self URLForLocalStoreDirectory]]; - if (![_persistentStoreCoordinator.persistentStores count]) - [self loadStore]; + [self.persistentStoreCoordinator unlock]; + [self loadStore]; - return _persistentStoreCoordinator; + return YES; } +#pragma mark - Properties + - (BOOL)cloudEnabled { NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; @@ -449,7 +683,7 @@ - (NSString *)storeUUID { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; NSString *storeUUID = [cloud objectForKey:StoreUUIDKey]; - + // If no storeUUID is set yet, create a new storeUUID and return that as long as no storeUUID is set yet. // When the migration to the new storeUUID is successful, we update the iCloud's KVS with a call to -setStoreUUID. if (!storeUUID) { @@ -465,12 +699,12 @@ - (NSString *)storeUUID { * When a tentativeStoreUUID is set, this operation confirms it and writes it as the new storeUUID to the iCloud KVS. */ - (void)confirmTentativeStoreUUID { - + if (self.tentativeStoreUUID) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; [cloud synchronize]; - + [self resetTentativeStoreUUID]; } } @@ -479,7 +713,7 @@ - (void)confirmTentativeStoreUUID { * When a tentativeStoreUUID is set, this operation resets it so that a new one will be generated if necessary. */ - (void)resetTentativeStoreUUID { - + self.tentativeStoreUUID = nil; } @@ -494,7 +728,7 @@ - (NSURL *)presentedItemURL { } -(NSOperationQueue *)presentedItemOperationQueue { - + return _presentedItemOperationQueue; } @@ -541,7 +775,7 @@ - (void)cloudStoreChanged:(NSNotification *)note { // Don't reload the store when the local one is active. if (!self.cloudEnabled) return; - + // Reload the store. [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, identityToken]; [self loadStore]; @@ -554,7 +788,7 @@ - (void)mergeChanges:(NSNotification *)note { NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; [moc performBlockAndWait:^{ [moc mergeChangesFromContextDidSaveNotification:note]; - + NSError *error = nil; if (![moc save:&error]) { [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; @@ -566,10 +800,8 @@ - (void)mergeChanges:(NSNotification *)note { } }]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self - userInfo:[note userInfo]]; - }); + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self + userInfo:[note userInfo]]; }]; } From 25c9da84b476339aed2c10c611d2d6640b93c61e Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 02:45:49 -0500 Subject: [PATCH 14/35] Example updated. [UPDATED] Example now uses new PSC API. [ADDED] Example now indicates loading progress. [UPDATED] Example now handles unavailable MOC. --- iCloudStoreManagerExample/AppDelegate.h | 1 - iCloudStoreManagerExample/AppDelegate.m | 75 +++++------- .../MasterViewController.h | 2 +- .../MasterViewController.m | 113 +++++++++--------- .../en.lproj/MasterViewController_iPad.xib | 57 +++++++-- .../en.lproj/MasterViewController_iPhone.xib | 57 +++++++-- .../iCloudStoreManagerExample.entitlements | 6 +- 7 files changed, 178 insertions(+), 133 deletions(-) diff --git a/iCloudStoreManagerExample/AppDelegate.h b/iCloudStoreManagerExample/AppDelegate.h index c0bdc83..e95fb85 100644 --- a/iCloudStoreManagerExample/AppDelegate.h +++ b/iCloudStoreManagerExample/AppDelegate.h @@ -17,7 +17,6 @@ @property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; -@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator; - (void)saveContext; - (NSURL *)applicationDocumentsDirectory; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index 7ec8092..fb2d90a 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -24,7 +24,6 @@ @implementation AppDelegate { @synthesize window = _window; @synthesize managedObjectContext = __managedObjectContext; @synthesize managedObjectModel = __managedObjectModel; -@synthesize persistentStoreCoordinator = __persistentStoreCoordinator; @synthesize navigationController = _navigationController; @synthesize splitViewController = _splitViewController; @synthesize ubiquityStoreManager; @@ -37,9 +36,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { // STEP 1 - Initialize the UbiquityStoreManager ubiquityStoreManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:[self managedObjectModel] - localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil]; - // STEP 2a - Setup the delegate - ubiquityStoreManager.delegate = self; + localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil + delegate:self]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. @@ -47,7 +45,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController_iPhone" bundle:nil]; self.navigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController]; self.window.rootViewController = self.navigationController; - masterViewController.managedObjectContext = self.managedObjectContext; } else { masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController_iPad" bundle:nil]; UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController]; @@ -62,7 +59,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( self.splitViewController.viewControllers = [NSArray arrayWithObjects:masterNavigationController, detailNavigationController, nil]; self.window.rootViewController = self.splitViewController; - masterViewController.managedObjectContext = self.managedObjectContext; } [self.window makeKeyAndVisible]; return YES; @@ -115,29 +111,6 @@ - (void)saveContext { #pragma mark - Core Data stack -// Returns the managed object context for the application. -// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application. -- (NSManagedObjectContext *)managedObjectContext { - - if (__managedObjectContext != nil) { - return __managedObjectContext; - } - - NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; - - if (coordinator != nil) { - NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; - - [moc performBlockAndWait:^{ - [moc setPersistentStoreCoordinator: coordinator]; - }]; - - __managedObjectContext = moc; - __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; - } - return __managedObjectContext; -} - // Returns the managed object model for the application. // If the model doesn't already exist, it is created from the application's model. - (NSManagedObjectModel *)managedObjectModel { @@ -153,18 +126,6 @@ - (NSURL *)storeURL { return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"]; } -// Returns the persistent store coordinator for the application. -// If the coordinator doesn't already exist, it is created and the application's store added to it. -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - if (__persistentStoreCoordinator == nil) { - - // STEP 3 - Get the persistentStoreCoordinator from the UbiquityStoreManager - __persistentStoreCoordinator = [ubiquityStoreManager persistentStoreCoordinator]; - } - - return __persistentStoreCoordinator; -} - #pragma mark - Application's Documents directory // Returns the URL to the application's Documents directory. @@ -190,13 +151,37 @@ - (User *)primaryUser { #pragma mark - UbiquityStoreManagerDelegate // STEP 4 - Implement the UbiquityStoreManager delegate methods +- (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager { + return self.managedObjectContext; +} + +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { + + dispatch_async(dispatch_get_main_queue(), ^{ + [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; + [masterViewController.storeLoadingActivity startAnimating]; + }); +} + +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore { -- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm { - return self.managedObjectContext; + dispatch_async(dispatch_get_main_queue(), ^{ + [masterViewController.storeLoadingActivity stopAnimating]; + }); + manager.cloudEnabled = NO; } -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)didSwitch { - [masterViewController.iCloudSwitch setOn:didSwitch animated:YES]; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore { + + NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; + [moc setPersistentStoreCoordinator:coordinator]; + + __managedObjectContext = moc; + __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; + dispatch_async(dispatch_get_main_queue(), ^{ + [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; + [masterViewController.storeLoadingActivity stopAnimating]; + }); } @end diff --git a/iCloudStoreManagerExample/MasterViewController.h b/iCloudStoreManagerExample/MasterViewController.h index 3b798e7..ba7b58b 100644 --- a/iCloudStoreManagerExample/MasterViewController.h +++ b/iCloudStoreManagerExample/MasterViewController.h @@ -17,10 +17,10 @@ @property (strong, nonatomic) DetailViewController *detailViewController; @property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController; -@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext; @property (strong, nonatomic) IBOutlet UISwitch *iCloudSwitch; @property (strong, nonatomic) IBOutlet UIView *tableHeaderView; @property (strong, nonatomic) IBOutlet UIButton *clearButton; +@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *storeLoadingActivity; - (IBAction)setiCloudState:(id)sender; - (IBAction)cleariCloud:(id)sender; diff --git a/iCloudStoreManagerExample/MasterViewController.m b/iCloudStoreManagerExample/MasterViewController.m index 8ce2c6c..4aad22f 100644 --- a/iCloudStoreManagerExample/MasterViewController.m +++ b/iCloudStoreManagerExample/MasterViewController.m @@ -20,7 +20,6 @@ @implementation MasterViewController @synthesize detailViewController = _detailViewController; @synthesize fetchedResultsController = __fetchedResultsController; -@synthesize managedObjectContext = __managedObjectContext; @synthesize iCloudSwitch; @synthesize clearButton; @synthesize tableHeaderView; @@ -75,25 +74,17 @@ - (void)viewDidLoad { UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)]; self.navigationItem.rightBarButtonItem = addButton; - // iCloud support - [self reloadFetchedResults:nil]; - // Observe the app delegate telling us when it's finished asynchronously setting up the persistent store [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(reloadFetchedResults:) name: UbiquityManagedStoreDidChangeNotification object: [[AppDelegate appDelegate] ubiquityStoreManager]]; - + self.tableView.tableHeaderView = self.tableHeaderView; // STEP 5c - Display current state of the UbiquityStoreManager self.iCloudSwitch.on = [[AppDelegate appDelegate] ubiquityStoreManager].cloudEnabled; -} - -- (void)viewDidUnload -{ - [super viewDidUnload]; - // Release any retained subviews of the main view. + [self reloadFetchedResults:nil]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation @@ -213,6 +204,10 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath #pragma mark - Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { + + if ([AppDelegate appDelegate].managedObjectContext == nil) { + return nil; + } if (__fetchedResultsController != nil) { return __fetchedResultsController; @@ -220,7 +215,7 @@ - (NSFetchedResultsController *)fetchedResultsController { NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. - NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext]; + NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:[AppDelegate appDelegate].managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. @@ -234,11 +229,11 @@ - (NSFetchedResultsController *)fetchedResultsController { // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". - NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"]; + NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[AppDelegate appDelegate].managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"]; aFetchedResultsController.delegate = self; self.fetchedResultsController = aFetchedResultsController; - [self.managedObjectContext performBlockAndWait:^{ + [[AppDelegate appDelegate].managedObjectContext performBlockAndWait:^{ NSError *error = nil; if (![self.fetchedResultsController performFetch:&error]) { // Replace this implementation with code to handle the error appropriately. @@ -251,54 +246,54 @@ - (NSFetchedResultsController *)fetchedResultsController { return __fetchedResultsController; } -- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller -{ - [self.tableView beginUpdates]; -} - -- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo - atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type -{ - switch(type) { - case NSFetchedResultsChangeInsert: - [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeDelete: - [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; - break; - } -} - -- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject - atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type - newIndexPath:(NSIndexPath *)newIndexPath -{ - UITableView *tableView = self.tableView; - - switch(type) { - case NSFetchedResultsChangeInsert: - [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeDelete: - [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; - break; - - case NSFetchedResultsChangeUpdate: - [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; - break; - - case NSFetchedResultsChangeMove: - [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; - [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade]; - break; - } -} +//- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller +//{ +// [self.tableView beginUpdates]; +//} +// +//- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo +// atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type +//{ +// switch(type) { +// case NSFetchedResultsChangeInsert: +// [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeDelete: +// [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// } +//} +// +//- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject +// atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type +// newIndexPath:(NSIndexPath *)newIndexPath +//{ +// UITableView *tableView = self.tableView; +// +// switch(type) { +// case NSFetchedResultsChangeInsert: +// [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeDelete: +// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; +// break; +// +// case NSFetchedResultsChangeUpdate: +// [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; +// break; +// +// case NSFetchedResultsChangeMove: +// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; +// [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade]; +// break; +// } +//} - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { - [self.tableView endUpdates]; + [self.tableView reloadData]; } /* diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib index f0893b5..bf3ecf3 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib +++ b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib @@ -1,22 +1,23 @@ - 1296 - 11D50 - 2182 - 1138.32 - 568.00 + 1552 + 12C60 + 3084 + 1187.34 + 625.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1179 + 2083 + IBProxyObject + IBUIActivityIndicatorView IBUIButton - IBUITableView + IBUILabel IBUISwitch + IBUITableView IBUIView - IBUILabel - IBProxyObject com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -113,6 +114,7 @@ 1 MCAwIDAAA + darkTextColor 0 @@ -159,6 +161,17 @@ 16 + + + -2147483356 + {{246, 24}, {20, 20}} + + + _NS:9 + NO + IBIPadFramework + 2 + {320, 130} @@ -209,6 +222,14 @@ 14 + + + storeLoadingActivity + + + + 16 + dataSource @@ -275,6 +296,7 @@ + Table Header View @@ -294,6 +316,11 @@ + + 15 + + + @@ -302,6 +329,7 @@ UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -311,7 +339,7 @@ - 14 + 16 @@ -335,6 +363,7 @@ UIButton UISwitch + UIActivityIndicatorView UIView @@ -346,6 +375,10 @@ iCloudSwitch UISwitch + + storeLoadingActivity + UIActivityIndicatorView + tableHeaderView UIView @@ -362,10 +395,10 @@ IBIPadFramework com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS - + YES 3 - 1179 + 2083 diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib index 3bff54c..d364a21 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib +++ b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib @@ -1,22 +1,23 @@ - 1296 - 11D50 - 2182 - 1138.32 - 568.00 + 1552 + 12C60 + 3084 + 1187.34 + 625.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 1179 + 2083 + IBProxyObject + IBUIActivityIndicatorView IBUIButton - IBUITableView + IBUILabel IBUISwitch + IBUITableView IBUIView - IBUILabel - IBProxyObject com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -90,6 +91,7 @@ 1 MCAwIDAAA + darkTextColor 0 @@ -136,6 +138,17 @@ 16 + + + -2147483356 + {{246, 24}, {20, 20}} + + + _NS:9 + NO + IBCocoaTouchFramework + 2 + {320, 130} @@ -186,6 +199,14 @@ 14 + + + storeLoadingActivity + + + + 16 + dataSource @@ -252,6 +273,7 @@ + Table Header View @@ -271,6 +293,11 @@ + + 15 + + + @@ -279,6 +306,7 @@ UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -288,7 +316,7 @@ - 14 + 16 @@ -312,6 +340,7 @@ UIButton UISwitch + UIActivityIndicatorView UIView @@ -323,6 +352,10 @@ iCloudSwitch UISwitch + + storeLoadingActivity + UIActivityIndicatorView + tableHeaderView UIView @@ -339,10 +372,10 @@ IBCocoaTouchFramework com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS - + YES 3 - 1179 + 2083 diff --git a/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements b/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements index fe995ce..dc91525 100644 --- a/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements +++ b/iCloudStoreManagerExample/iCloudStoreManagerExample.entitlements @@ -4,13 +4,13 @@ com.apple.developer.ubiquity-container-identifiers - $(TeamIdentifierPrefix)com.yodelcode.iCloudManager + $(TeamIdentifierPrefix)com.lyndir.lhunath.iCloudManager com.apple.developer.ubiquity-kvstore-identifier - $(TeamIdentifierPrefix)com.yodelcode.iCloudManager + $(TeamIdentifierPrefix)com.lyndir.lhunath.iCloudManager keychain-access-groups - $(AppIdentifierPrefix)com.yodelcode.iCloudManager + $(AppIdentifierPrefix)com.lyndir.lhunath.iCloudManager From 1724a43fb2362083af6ceb1be889efa7ff3acc7f Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 02:56:09 -0500 Subject: [PATCH 15/35] Some doc updates + put all notifications on the main thread. [UPDATED] Docs for delegate methods with usage recommendations. --- iCloudStoreManager/UbiquityStoreManager.h | 20 +++++++++++++------- iCloudStoreManager/UbiquityStoreManager.m | 6 ++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 8ee132a..a1bbd8f 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -53,22 +53,28 @@ typedef enum { @required - (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager; +/** Triggered when the store manager begins loading a persistence store. + * + * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:, the application should not be using the persistence coordinator. + * You should probably unset your managed object contexts here to prevent exceptions/hangs in your applications (the coordinator is locked and its store removed). + * Also useful for indicating in your user interface that the store is loading. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; /** Triggered when the store manager loads a persistence store. * * This is where you'll init/update your application's persistence layer. + * You should probably create your main managed object context here. Note the coordinator could change during the application's lifetime. */ @required - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; - -/** Triggered when the store manager begins loading a persistence store. +/** Triggered when the store manager fails to loads a persistence store. * - * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:, the application should not be using the persistence coordinator. Ideally, you could unset your managed object contexts here. - * Also useful for indicating in your user interface that the store is loading. + * Useful to decide what to do to make a store available to the application. + * You should probably unset your managed object contexts here to prevent exceptions in your applications (the coordinator has no more store). + * If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. You can implement this simply with `manager.cloudEnabled = NO;`. */ @optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; -/** Triggered when the store manager fails to loads a persistence store. Useful to decide what to do to make a store available to the application. If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. */ -@optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore; /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ @optional diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 3b93f06..0953054 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -800,8 +800,10 @@ - (void)mergeChanges:(NSNotification *)note { } }]; - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self - userInfo:[note userInfo]]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self + userInfo:[note userInfo]]; + }); }]; } From f92417dc396c07369fe61c3dda8ac4aedf101025 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 14:01:14 -0500 Subject: [PATCH 16/35] Fix copy migration. [ADDED] Also log detailed errors. [FIXED] Create the cloud content directory if it doesn't exist yet. [FIXED] Same destination MOC after copy migration. --- iCloudStoreManager/UbiquityStoreManager.m | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 0953054..3b23ac0 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -165,8 +165,13 @@ - (void)error:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause conte if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:cause context:context]; - else + else { [self log:@"error: %@, cause: %u, context: %@", error, cause, context]; + + NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; + for (NSError *detailedError in detailedErrors) + [self log:@" - detailed error: %@", detailedError]; + } } #pragma mark - Store Management @@ -228,6 +233,9 @@ - (void)loadCloudStore { if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path withIntermediateDirectories:YES attributes:nil error:&error]) [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudContent].path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudContent].path]; // Add cloud store to PSC. NSURL *cloudStoreURL = [self URLForCloudStore]; @@ -270,7 +278,7 @@ - (void)loadCloudStore { options:cloudStoreOptions error:&error]; if (!cloudStore) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; break; } @@ -304,6 +312,11 @@ - (void)loadCloudStore { [self copyMigrateObject:localObject toContext:cloudContext usingMigrationCache:migratedIDsBySourceID]; } + // Save migrated entities. + if (!migrationFailure) + if (![cloudContext save:&error]) + migrationFailure = YES; + // Handle failure by cleaning up the cloud store. if (migrationFailure) { [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; @@ -410,6 +423,7 @@ - (void)loadCloudStore { [self observeStore]; } else { + // TODO: If this happens, the cloud store on this device is desynced. We should destroy it either locally or ubiquitously. [self resetTentativeStoreUUID]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { @@ -790,14 +804,9 @@ - (void)mergeChanges:(NSNotification *)note { [moc mergeChangesFromContextDidSaveNotification:note]; NSError *error = nil; - if (![moc save:&error]) { + if (![moc save:&error]) + // TODO: If this happens, the cloud store on this device is desynced. We should destroy it either locally or ubiquitously. [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; - - NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; - if ([detailedErrors count]) - for (NSError *detailedError in detailedErrors) - [self error:detailedError cause:UbiquityStoreManagerErrorCauseImportChanges context:nil]; - } }]; dispatch_async(dispatch_get_main_queue(), ^{ From 210e8ec44a7c839ba4f9221b98a62db22fc11147 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 21:57:06 -0500 Subject: [PATCH 17/35] Rebuilding of the local cloud container. [ADDED] Support deletion of the cloud data on the device only (allowing it to be rebuilt from the iCloud data). [IMPROVED] More verbose error printing. [IMPROVED] Send willLoadStoreIsCloud to application when clearing the store so it knows the store is unavailable while it's being deleted too (in fact, whenever the PSC is cleared). --- iCloudStoreManager/UbiquityStoreManager.h | 52 +++--- iCloudStoreManager/UbiquityStoreManager.m | 63 +++++--- .../MasterViewController.h | 1 + .../MasterViewController.m | 7 +- .../en.lproj/MasterViewController_iPad.xib | 148 ++++++++---------- .../en.lproj/MasterViewController_iPhone.xib | 92 +++++++---- 6 files changed, 205 insertions(+), 158 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index a1bbd8f..dab4a08 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -61,6 +61,7 @@ typedef enum { */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; + /** Triggered when the store manager loads a persistence store. * * This is where you'll init/update your application's persistence layer. @@ -68,6 +69,7 @@ typedef enum { */ @required - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; + /** Triggered when the store manager fails to loads a persistence store. * * Useful to decide what to do to make a store available to the application. @@ -76,13 +78,16 @@ typedef enum { */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore; + /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; + /** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; + /** Triggered when the store manager needs to perform a manual store migration. * @param error Write out an error object here when the migration fails. * @return YES when the migration was successful and the new store may be loaded. NO to error out and not load the new store (new store will be cleaned up if it exists). @@ -117,49 +122,54 @@ typedef enum { containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions delegate:(id)delegate; /** - * This will delete all the data from iCloud for this application. There is no recovery. A new iCloud store will be created if enabled. + * This will delete all the data from iCloud for this application. + * + * @param localOnly If YES, the iCloud data will be redownloaded when needed. If NO, the container's data will be permanently lost. + * + * Unless you intend to delete more than just the active cloud store, you should probably use -deleteCloudStoreLocalOnly: instead. */ -- (BOOL)nukeCloudContainer; +- (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly; /** - * This will delete the local store. There is no recovery. + * This will delete the iCloud store. + * + * @param localOnly If YES, the iCloud transaction logs will be redownloaded and the store rebuilt. If NO, the store will be permanently lost and a new one will be created by migrating the device's local store. */ -- (BOOL)deleteLocalStore; +- (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly; /** - * This will delete the iCloud store. Theoretically, it should be rebuilt from the iCloud transaction logs. - * TODO: Verify claim. + * This will delete the local store. There is no recovery. */ -- (BOOL)deleteCloudStore; +- (BOOL)deleteLocalStore; /** -* Determine whether it's safe to seed the cloud store with a local store. -*/ + * Determine whether it's safe to seed the cloud store with a local store. + */ - (BOOL)cloudSafeForSeeding; /** -* @return URL to the active app's ubiquity container. -*/ + * @return URL to the active app's ubiquity container. + */ - (NSURL *)URLForCloudContainer; /** -* @return URL to the directory where we put cloud store databases for this app. -*/ + * @return URL to the directory where we put cloud store databases for this app. + */ - (NSURL *)URLForCloudStoreDirectory; /** -* @return URL to the active cloud store's database. -*/ + * @return URL to the active cloud store's database. + */ - (NSURL *)URLForCloudStore; /** -* @return URL to the directory where we put cloud store transaction logs for this app. -*/ + * @return URL to the directory where we put cloud store transaction logs for this app. + */ - (NSURL *)URLForCloudContentDirectory; /** -* @return URL to the active cloud store's transaction logs. -*/ + * @return URL to the active cloud store's transaction logs. + */ - (NSURL *)URLForCloudContent; /** @@ -168,8 +178,8 @@ typedef enum { - (NSURL *)URLForLocalStoreDirectory; /** -* @return URL to the local store's database. -*/ + * @return URL to the local store's database. + */ - (NSURL *)URLForLocalStore; @end diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 3b23ac0..bb1f253 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -166,11 +166,16 @@ - (void)error:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause conte if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:cause context:context]; else { - [self log:@"error: %@, cause: %u, context: %@", error, cause, context]; + [self log:@"Error (cause:%u): %@", cause, error]; + if (context) + [self log:@" - Context : %@", context]; + NSError *underlyingError = [[error userInfo] objectForKey:NSUnderlyingErrorKey]; + if (underlyingError) + [self log:@" - Underlying: %@", underlyingError]; NSArray *detailedErrors = [[error userInfo] objectForKey:NSDetailedErrorsKey]; for (NSError *detailedError in detailedErrors) - [self log:@" - detailed error: %@", detailedError]; + [self log:@" - Detail : %@", detailedError]; } } @@ -178,6 +183,9 @@ - (void)error:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause conte - (void)clearStore { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) + [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; + // Remove store observers. [NSFileCoordinator removeFilePresenter:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; @@ -323,7 +331,7 @@ - (void)loadCloudStore { if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) [self error:error cause:cause = UbiquityStoreManagerErrorCauseClearStore context:cloudStoreURL.path]; - [self removeItemAtURL:cloudStoreURL]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } @@ -372,7 +380,7 @@ - (void)loadCloudStore { manuallyMigrateStore:localStoreURL withOptions:localStoreOptions toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; - [self removeItemAtURL:cloudStoreURL]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } @@ -589,7 +597,7 @@ - (void)observeStore { #endif } -- (void)removeItemAtURL:(NSURL *)directoryURL { +- (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { NSError *error = nil; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL @@ -597,57 +605,66 @@ - (void)removeItemAtURL:(NSURL *)directoryURL { error:&error byAccessor: ^(NSURL *newURL) { NSError *error_ = nil; - if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + if (localOnly && [[NSFileManager defaultManager] isUbiquitousItemAtURL:newURL]) { + if (![[NSFileManager defaultManager] evictUbiquitousItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + } else { + if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) + [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + } }]; if (error) [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:directoryURL.path]; } -- (BOOL)nukeCloudContainer { +- (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly { if (![self.persistentStoreCoordinator tryLock]) { - [self log:@"Cannot nuke the cloud container: Manager is locked."]; + [self log:@"Cannot delete the cloud container: Manager is locked."]; return NO; } - [self log:@"Will nuke the cloud container."]; + [self log:@"Will delete the cloud container %@.", localOnly? @"on this device": @"on this device and in the cloud"]; [self clearStore]; // Delete the whole cloud container. - [self removeItemAtURL:[self URLForCloudContainer]]; + [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; // Unset the storeUUID so a new one will be created. [self resetTentativeStoreUUID]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + } + [self.persistentStoreCoordinator unlock]; [self loadStore]; return YES; } -- (BOOL)deleteCloudStore { +- (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly { if (![self.persistentStoreCoordinator tryLock]) { [self log:@"Cannot delete the cloud store: Manager is locked."]; return NO; } - [self log:@"Will delete the cloud store (UUID:%@).", self.storeUUID]; + [self log:@"Will delete the cloud store (UUID:%@) %@.", self.storeUUID, localOnly ? @"on this device" : @"on this device and in the cloud"]; [self clearStore]; // Clean up any cloud stores and transaction logs. - [self removeItemAtURL:[self URLForCloudStoreDirectory]]; - [self removeItemAtURL:[self URLForCloudContentDirectory]]; + [self removeItemAtURL:[self URLForCloudStoreDirectory] localOnly:localOnly]; + [self removeItemAtURL:[self URLForCloudContentDirectory] localOnly:localOnly]; // Unset the storeUUID so a new one will be created. [self resetTentativeStoreUUID]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + } [self.persistentStoreCoordinator unlock]; [self loadStore]; @@ -666,7 +683,7 @@ - (BOOL)deleteLocalStore { [self clearStore]; // Remove just the local store. - [self removeItemAtURL:[self URLForLocalStoreDirectory]]; + [self removeItemAtURL:[self URLForLocalStoreDirectory] localOnly:YES]; [self.persistentStoreCoordinator unlock]; [self loadStore]; diff --git a/iCloudStoreManagerExample/MasterViewController.h b/iCloudStoreManagerExample/MasterViewController.h index ba7b58b..b7ef5bc 100644 --- a/iCloudStoreManagerExample/MasterViewController.h +++ b/iCloudStoreManagerExample/MasterViewController.h @@ -24,5 +24,6 @@ - (IBAction)setiCloudState:(id)sender; - (IBAction)cleariCloud:(id)sender; +- (IBAction)rebuildiCloud:(id)sender; @end diff --git a/iCloudStoreManagerExample/MasterViewController.m b/iCloudStoreManagerExample/MasterViewController.m index 4aad22f..29b3fe0 100644 --- a/iCloudStoreManagerExample/MasterViewController.m +++ b/iCloudStoreManagerExample/MasterViewController.m @@ -34,7 +34,12 @@ - (IBAction)setiCloudState:(id)sender { - (IBAction)cleariCloud:(id)sender { // STEP 6 - UbiquityStoreManager hard reset. FOR TESTING ONLY! Do not expose to the end user! - [[[AppDelegate appDelegate] ubiquityStoreManager] nukeCloudContainer]; + [[[AppDelegate appDelegate] ubiquityStoreManager] deleteCloudContainerLocalOnly:NO]; +} + +- (IBAction)rebuildiCloud:(id)sender { + + [[[AppDelegate appDelegate] ubiquityStoreManager] deleteCloudContainerLocalOnly:YES]; } - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib index bf3ecf3..2050f3f 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib +++ b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPad.xib @@ -40,7 +40,6 @@ 274 {{0, 20}, {320, 832}} - 3 MQA @@ -86,10 +85,9 @@ 292 - {{146, 20}, {94, 27}} + {{156, 20}, {94, 27}} - - + _NS:9 NO IBIPadFramework @@ -100,9 +98,8 @@ 292 - {{81, 22}, {129, 24}} + {{30, 22}, {123, 24}} - _NS:9 NO @@ -119,6 +116,7 @@ 0 10 + 2 2 20 @@ -129,53 +127,72 @@ 16 - + + + -2147483356 + {{256, 24}, {20, 20}} + + _NS:9 + NO + IBIPadFramework + 2 + + 292 - {{81, 73}, {159, 37}} + {{171, 67}, {130, 44}} - _NS:9 NO IBIPadFramework 0 0 1 - Clear iCloud Data + Rebuild iCloud 1 MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - + 3 MC41AA - + 2 15 - + Helvetica-Bold 15 16 - + - -2147483356 - {{246, 24}, {20, 20}} + 292 + {{40, 67}, {113, 44}} - + _NS:9 NO IBIPadFramework - 2 + 0 + 0 + 1 + Clear iCloud + + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + + {320, 130} - _NS:9 @@ -214,14 +231,6 @@ 10 - - - clearButton - - - - 14 - storeLoadingActivity @@ -255,14 +264,23 @@ 11 + + + rebuildiCloud: + + + 7 + + 20 + cleariCloud: - + 7 - 13 + 19 @@ -295,7 +313,8 @@ - + + @@ -312,13 +331,18 @@ - 12 - + 15 + - 15 - + 17 + + + + + 18 + @@ -328,8 +352,9 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -339,58 +364,9 @@ - 16 - - - - - MasterViewController - UITableViewController - - id - id - - - - cleariCloud: - id - - - setiCloudState: - id - - - - UIButton - UISwitch - UIActivityIndicatorView - UIView - - - - clearButton - UIButton - - - iCloudSwitch - UISwitch - - - storeLoadingActivity - UIActivityIndicatorView - - - tableHeaderView - UIView - - - - IBProjectSource - ./Classes/MasterViewController.h - - - + 20 + 0 IBIPadFramework diff --git a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib index d364a21..1a085a0 100644 --- a/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib +++ b/iCloudStoreManagerExample/en.lproj/MasterViewController_iPhone.xib @@ -63,10 +63,10 @@ 292 - {{146, 20}, {94, 27}} + {{156, 20}, {94, 27}} - + _NS:9 NO IBCocoaTouchFramework @@ -77,7 +77,7 @@ 292 - {{81, 22}, {129, 24}} + {{24, 22}, {129, 24}} @@ -96,6 +96,7 @@ 0 10 + 2 2 20 @@ -106,10 +107,10 @@ 16 - + 292 - {{81, 73}, {159, 37}} + {{171, 67}, {130, 44}} _NS:9 @@ -118,32 +119,56 @@ 0 0 1 - Clear iCloud Data + Rebuild iCloud 1 MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA - + 3 MC41AA - + 2 15 - + Helvetica-Bold 15 16 + + + 292 + {{40, 67}, {113, 44}} + + + + _NS:9 + NO + IBCocoaTouchFramework + 0 + 0 + 1 + Clear iCloud + + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + + + -2147483356 - {{246, 24}, {20, 20}} + {{256, 24}, {20, 20}} + _NS:9 NO IBCocoaTouchFramework @@ -191,14 +216,6 @@ 10 - - - clearButton - - - - 14 - storeLoadingActivity @@ -232,14 +249,23 @@ 11 + + + rebuildiCloud: + + + 7 + + 23 + cleariCloud: - + 7 - 13 + 22 @@ -270,9 +296,10 @@ 6 - + + @@ -289,13 +316,18 @@ - 12 - + 15 + - 15 - + 17 + + + + + 21 + @@ -305,9 +337,10 @@ com.apple.InterfaceBuilder.IBCocoaTouchPlugin UIResponder com.apple.InterfaceBuilder.IBCocoaTouchPlugin - com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -316,7 +349,7 @@ - 16 + 23 @@ -325,6 +358,7 @@ UITableViewController id + id id @@ -332,6 +366,10 @@ cleariCloud: id + + rebuildiCloud: + id + setiCloudState: id From c4d2f4ba62e675ecaad7cea4f8eb9432b2557ecb Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 4 Mar 2013 22:10:22 -0500 Subject: [PATCH 18/35] Improved store clearing on open error and do store reloading on import error. [UPDATED] Use clearStore to clear the PSC on error to make absolutely sure it's cleared. [FIXED] When a log import fails, reload the store to see if it's still viable to prevent the illusion that iCloud is still enabled and working while in reality it's broken. --- iCloudStoreManager/UbiquityStoreManager.m | 30 ++++++++++------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index bb1f253..4bb4ed6 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -339,9 +339,8 @@ - (void)loadCloudStore { if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:cloudStoreURL options:cloudStoreOptions - error:&error]) { + error:&error]) [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; - } break; } @@ -366,9 +365,7 @@ - (void)loadCloudStore { withType:NSSQLiteStoreType error:&error]) { [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; - - if (![self.persistentStoreCoordinator removePersistentStore:localStore error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseClearStore context:cloudStoreURL.path]; + [self clearStore]; } break; } @@ -388,9 +385,8 @@ - (void)loadCloudStore { if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:cloudStoreURL options:cloudStoreOptions - error:&error]) { + error:&error]) [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; - } break; } @@ -402,9 +398,8 @@ - (void)loadCloudStore { if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:cloudStoreURL options:cloudStoreOptions - error:&error]) { + error:&error]) [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; - } break; } } @@ -417,11 +412,7 @@ - (void)loadCloudStore { [userInfo setObject:error forKey:NSUnderlyingErrorKey]; [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:exception]; - - // Clean up any local stores that may still be present. - for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) - if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseClearStore context:store]; + [self clearStore]; } @finally { BOOL cloudWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; @@ -431,7 +422,7 @@ - (void)loadCloudStore { [self observeStore]; } else { - // TODO: If this happens, the cloud store on this device is desynced. We should destroy it either locally or ubiquitously. + // TODO: If this happens, the cloud store is desynced. Until we destroy it or fix it (seems impossible), iCloud will be unavailable to the user. [self resetTentativeStoreUUID]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { @@ -821,9 +812,14 @@ - (void)mergeChanges:(NSNotification *)note { [moc mergeChangesFromContextDidSaveNotification:note]; NSError *error = nil; - if (![moc save:&error]) - // TODO: If this happens, the cloud store on this device is desynced. We should destroy it either locally or ubiquitously. + if (![moc save:&error]) { + // TODO: If this happens, the cloud store is desynced. Until we destroy it or fix it (seems impossible), iCloud will be unavailable to the user. [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; + + // Try to reload the store to see if it's still viable. + // If not, either the application will handle it or we'll fall back to the local store. + [self loadStore]; + } }]; dispatch_async(dispatch_get_main_queue(), ^{ From a03e8c0a8d0cf067af17df6c3f8cf081d794105e Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 7 Mar 2013 01:25:02 -0500 Subject: [PATCH 19/35] Desync avoidance. [ADDED] Yield the context of errors in failedLoadingStoreWithCause:. [UPDATED] Documentation for error causes specify what the context will be. [ADDED] Desync avoidance: An iOS bug causes the cloud store to break irreparably when validation relationships are modified on two devices simultaneously. Desync avoidance avoids the issue from occurring by ensuring only one device can modify the cloud store at a time. The developer has a choice between different strategies that determine what to do when the device cannot obtain exclusive access (ie. another device has exclusive access). [MOVED] The current identity is now stored as state in the manager and not persisted in the user defaults. There is no need to persist it any longer than the store is open for. --- iCloudStoreManager/UbiquityStoreManager.h | 66 ++++-- iCloudStoreManager/UbiquityStoreManager.m | 242 ++++++++++++++++------ iCloudStoreManagerExample/AppDelegate.m | 2 +- 3 files changed, 226 insertions(+), 84 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index dab4a08..badc9d9 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -23,24 +23,40 @@ extern NSString *const UbiquityManagedStoreDidChangeNotification; * The store managed by the ubiquity manager's coordinator imported changes from iCloud (eg. another device saved changes to iCloud). */ extern NSString *const UbiquityManagedStoreDidImportChangesNotification; +/** + * The key at which the UUID of the device that is inhibiting exclusive access to the store can be found in the context of UbiquityStoreErrorCauseNoExclusiveAccess. + */ +extern NSString *const UbiquityManagedStoreExclusiveDeviceUUIDKey; +/** + * The key at which the name of the device that is inhibiting exclusive access to the store can be found in the context of UbiquityStoreErrorCauseNoExclusiveAccess. + */ +extern NSString *const UbiquityManagedStoreExclusiveDeviceNameKey; + +typedef enum { + UbiquityStoreErrorCauseNoAccount, // The user is not logged into iCloud on this device. There is no context. + UbiquityStoreErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. context = the path of the store. + UbiquityStoreErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. context = the path of the store. + UbiquityStoreErrorCauseClearStore, // Error occurred while removing a store from the coordinator. context = the store. + UbiquityStoreErrorCauseOpenLocalStore, // Error occurred while opening the local store file. context = the path of the store. + UbiquityStoreErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. context = the path of the store. + UbiquityStoreErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. context = the path of the store or exception that caused the problem. + UbiquityStoreErrorCauseImportChanges, // Error occurred while importing changes from the cloud into the application's context. context = the DidImportUbiquitousContentChanges notification. + UbiquityStoreErrorCauseNoExclusiveAccess // This device was unable to obtain exclusive access to the store. context = a dictionary with keys UbiquityManagedStoreExclusiveDeviceUUIDKey, UbiquityManagedStoreExclusiveDeviceNameKey. +} UbiquityStoreErrorCause; typedef enum { - UbiquityStoreManagerErrorCauseNoAccount, // The user is not logged into iCloud on this device. - UbiquityStoreManagerErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. - UbiquityStoreManagerErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. - UbiquityStoreManagerErrorCauseClearStore, // Error occurred while removing the active store from the coordinator. - UbiquityStoreManagerErrorCauseOpenLocalStore, // Error occurred while opening the local store file. - UbiquityStoreManagerErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. - UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. - UbiquityStoreManagerErrorCauseImportChanges // Error occurred while importing changes from the cloud into the application's context. -} UbiquityStoreManagerErrorCause; + UbiquityStoreMigrationStrategyCopyEntities, // Migrate by copying all entities from the active store to the new store. + UbiquityStoreMigrationStrategyIOS, // Migrate using iOS' migration routines (bugged for: cloud -> local on iOS 6.0, local -> cloud on iOS 6.1). + UbiquityStoreMigrationStrategyManual, // Migrate using the delegate's -ubiquityStoreManager:manuallyMigrateStore:toStore:. + UbiquityStoreMigrationStrategyNone, // Don't migrate, just create an empty destination store. +} UbiquityStoreMigrationStrategy; typedef enum { - UbiquityStoreManagerMigrationStrategyCopyEntities, // Migrate by copying all entities from the active store to the new store. - UbiquityStoreManagerMigrationStrategyIOS, // Migrate using iOS' migration routines (bugged for: cloud -> local on iOS 6.0, local -> cloud on iOS 6.1). - UbiquityStoreManagerMigrationStrategyManual, // Migrate using the delegate's -ubiquityStoreManager:manuallyMigrateStore:toStore:. - UbiquityStoreManagerMigrationStrategyNone, // Don't migrate, just create an empty destination store. -} UbiquityStoreManagerMigrationStrategy; + UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess, // Avoid cloud desync by requesting exclusive access to the cloud store and failing to load the store if another device has it. Persistence will be unavailable while another device has exclusive access. + UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess, // Avoid cloud desync by requesting exclusive access to the cloud store and opening the store read-only if another device has it. Persistence will be read-only while another device has exclusive access. + UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal, // Avoid cloud desync by requesting exclusive access to the cloud store and migrating it to the local store if another device has it. Persistence will be read-write but changes won't be synced while another device has exclusive access. + UbiquityStoreDesyncAvoidanceStrategyNone, // Don't try to avoid cloud desync. If it happens, your application can try and cope with it from -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:. +} UbiquityStoreDesyncAvoidanceStrategy; @class UbiquityStoreManager; @@ -75,14 +91,18 @@ typedef enum { * Useful to decide what to do to make a store available to the application. * You should probably unset your managed object contexts here to prevent exceptions in your applications (the coordinator has no more store). * If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. You can implement this simply with `manager.cloudEnabled = NO;`. + * + * IMPORTANT: When this method is triggered, the store is likely irreparably broken. + * Unless your application has a way to recover, you should probably delete the store in question (cloud/local). + * Until you do, the user will remain unable to use that store. */ @optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore; /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error - cause:(UbiquityStoreManagerErrorCause)cause context:(id)context; + cause:(UbiquityStoreErrorCause)cause context:(id)context; /** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ @optional @@ -101,13 +121,19 @@ typedef enum { @interface UbiquityStoreManager : NSObject -// The delegate provides the managed object context to use and is informed of events in the ubiquity manager. +/** The delegate provides the managed object context to use and is informed of events in the ubiquity manager. */ @property (nonatomic, weak) id delegate; -// Determines what strategy to use when migrating from one store to another (eg. local -> cloud). -@property (nonatomic, assign) UbiquityStoreManagerMigrationStrategy migrationStrategy; +/** Determines what strategy to use when migrating from one store to another (eg. local -> cloud). Default is UbiquityStoreMigrationStrategyCopyEntities. */ +@property (nonatomic, assign) UbiquityStoreMigrationStrategy migrationStrategy; + +/** Determines what strategy to use to avoid causing the cloud store on different devices to get desynced. Default is UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess. + * + * Because of bugs in iOS' iCloud implementation, desyncs happen when two devices simultaneously mutate a relationship. + */ +@property (nonatomic, assign) UbiquityStoreDesyncAvoidanceStrategy desyncAvoidanceStrategy; -// Indicates whether the iCloud store or the local store is in use. +/** Indicates whether the iCloud store or the local store is in use. */ @property (nonatomic) BOOL cloudEnabled; /** Start managing an optionally ubiquitous store coordinator. diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 4bb4ed6..7886c23 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -18,9 +18,14 @@ NSString *const UbiquityManagedStoreDidChangeNotification = @"UbiquityManagedStoreDidChangeNotification"; NSString *const UbiquityManagedStoreDidImportChangesNotification = @"UbiquityManagedStoreDidImportChangesNotification"; -NSString *const StoreUUIDKey = @"StoreUUIDKey"; -NSString *const CloudEnabledKey = @"CloudEnabledKey"; -NSString *const CloudIdentityKey = @"CloudIdentityKey"; +NSString *const UbiquityManagedStoreExclusiveDeviceUUIDKey = @"UbiquityManagedStoreExclusiveDeviceUUIDKey"; +NSString *const UbiquityManagedStoreExclusiveDeviceNameKey = @"UbiquityManagedStoreExclusiveDeviceNameKey"; +NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. +NSString *const DeviceUUIDKey = @"USMDeviceUUIDKey"; // local: The UUID of this device when checking exclusive access. +NSString *const StoreAccessChoosingKey = @"USMStoreAccessChoosingKey"; // cloud: device UUID -> whether that device is choosing a ticket. +NSString *const StoreAccessTicketKey = @"USMStoreAccessTicketKey"; // cloud: device UUID -> the ticket owned by that device. +NSString *const StoreAccessNameKey = @"USMStoreAccessNameKey"; // cloud: device UUID -> the name of that device. +NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. NSString *const CloudStoreDirectory = @"CloudStore.nosync"; NSString *const CloudLogsDirectory = @"CloudLogs"; @@ -36,6 +41,8 @@ @interface UbiquityStoreManager () @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; +@property(nonatomic) BOOL haveExclusiveAccess; +@property(nonatomic, strong) id currentIdentityToken; @end @@ -63,6 +70,8 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; // Private vars + _migrationStrategy = UbiquityStoreMigrationStrategyCopyEntities; + _desyncAvoidanceStrategy = UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess; _persistentStorageQueue = [NSOperationQueue new]; _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass([self class])]; _persistentStorageQueue.maxConcurrentOperationCount = 1; @@ -98,7 +107,7 @@ - (NSURL *)URLForApplicationContainer { NSError *error = nil; if (![[NSFileManager defaultManager] createDirectoryAtURL:applicationSupportURL withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseCreateStorePath context:applicationSupportURL.path]; + [self error:error cause:UbiquityStoreErrorCauseCreateStorePath context:applicationSupportURL.path]; return applicationSupportURL; #endif @@ -161,7 +170,7 @@ - (void)log:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2) { NSLog(@"UbiquityStoreManager: %@", message); } -- (void)error:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context { +- (void)error:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id)context { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didEncounterError:cause:context:)]) [self.delegate ubiquityStoreManager:self didEncounterError:error cause:cause context:context]; @@ -194,7 +203,7 @@ - (void)clearStore { NSError *error = nil; for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) - [self error:error cause:UbiquityStoreManagerErrorCauseClearStore context:store]; + [self error:error cause:UbiquityStoreErrorCauseClearStore context:store]; if ([self.persistentStoreCoordinator.persistentStores count]) { // We couldn't remove all the stores, make a new PSC instead. @@ -202,6 +211,16 @@ - (void)clearStore { self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; [self.persistentStoreCoordinator lock]; } + + // Store cleared. Relinquish exclusive access if we have it. + if (self.haveExclusiveAccess) { + // TODO: Can a device inadvertently stop using the store without relinquishing its ticket? Probably. Must handle. + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + NSString *ownUUID = [local objectForKey:DeviceUUIDKey]; + if (ownUUID) + [cloud setValue:@0 forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]]; + } } - (void)loadStore { @@ -227,26 +246,116 @@ - (void)loadCloudStore { return; NSError *error = nil; - UbiquityStoreManagerErrorCause cause; + UbiquityStoreErrorCause cause; + id context = nil; @try { [self clearStore]; // Check if the user is logged into iCloud on the device. if (![self URLForCloudContainer]) { - cause = UbiquityStoreManagerErrorCauseNoAccount; + cause = UbiquityStoreErrorCauseNoAccount; return; } + // Check for exclusive access. + if (self.desyncAvoidanceStrategy != UbiquityStoreDesyncAvoidanceStrategyNone) { + // Try to obtain exclusive access for this device based on Lamport's bakery algorithm. + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + + // Get the UUID of our device. + NSString *ownUUID = [local objectForKey:DeviceUUIDKey]; + if (!ownUUID) + [local setObject:ownUUID = [[NSUUID UUID] UUIDString] forKey:DeviceUUIDKey]; + + // Check whether we have a ticket (and let other devices know our device name). + int ownTicket = [[cloud valueForKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]] intValue]; + [cloud setObject:[[UIDevice currentDevice] name] forKey:[NSString stringWithFormat:@"%@.%@", StoreAccessNameKey, ownUUID]]; + + // If we don't have a ticket yet, choose one. + if (!ownTicket) { + [cloud setValue:@YES + forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessChoosingKey, ownUUID]]; + [cloud synchronize]; + ownTicket = [[cloud valueForKeyPath:[NSString stringWithFormat:@"%@@max", StoreAccessTicketKey]] intValue] + 1; + [cloud setValue:@(ownTicket) + forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]]; + [cloud setValue:@NO + forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessChoosingKey, ownUUID]]; + [cloud synchronize]; + } + + // Check to see if our ticket gives us access to the store. + NSString *exclusiveDeviceUUID = nil; + NSDictionary *tickets = [cloud dictionaryForKey:StoreAccessTicketKey]; + NSDictionary *choosing = [cloud dictionaryForKey:StoreAccessChoosingKey]; + for (NSString *deviceUUID in [tickets allKeys]) + if (![deviceUUID isEqualToString:ownUUID]) { + if ([[choosing objectForKey:deviceUUID] boolValue]) { + // Another device is picking a ticket, we can't assert access yet. + exclusiveDeviceUUID = deviceUUID; + break; + } + + int deviceTicket = [[tickets objectForKey:deviceUUID] intValue]; + if (deviceTicket && (deviceTicket < ownTicket || (deviceTicket == ownTicket && [deviceUUID compare:ownUUID] == NSOrderedAscending))) { + // Another device has a ticket that comes before ours (or is the same as ours but their UUID sorts first). + // We can't assert access until this device relinquishes their ticket. + exclusiveDeviceUUID = deviceUUID; + break; + } + } + + if (!exclusiveDeviceUUID) + // No device is inhibiting exclusive access. Our ticket will assert our access to the other devices. + self.haveExclusiveAccess = YES; + + else { + // Another device is inhibiting exclusive access for now. + // Let the strategy decide what to do in the mean time. + switch (self.desyncAvoidanceStrategy) { + case UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess: { + // Fail loading the store. + cause = UbiquityStoreErrorCauseNoExclusiveAccess; + context = @{ + UbiquityManagedStoreExclusiveDeviceUUIDKey : exclusiveDeviceUUID, + UbiquityManagedStoreExclusiveDeviceNameKey : + [cloud valueForKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessNameKey, exclusiveDeviceUUID]] + }; + return; + } + case UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess: { + // Open the store read-only. + // TODO: Beware: this may cause trouble when importing ubiquity changes. + [NSException raise:NSGenericException + format:@"Strategy not yet implemented: UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess"]; + break; + } + case UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal: { + // Migrate/copy our cloud store file to the local store and open that one instead. + // TODO: Beware: if we set cloudEnabled = NO, we won't detect when we do gain exclusive access. + [NSException raise:NSGenericException + format:@"Strategy not yet implemented: UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal"]; + break; + } + case UbiquityStoreDesyncAvoidanceStrategyNone: + [NSException raise:NSInternalInconsistencyException + format:@"Strategy doesn't need exclusive access: UbiquityStoreDesyncAvoidanceStrategyNone"]; + } + } + } + // Create the path to the cloud store. if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForCloudStoreDirectory].path]; if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudContent].path withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudContent].path]; + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForCloudContent].path]; // Add cloud store to PSC. NSURL *cloudStoreURL = [self URLForCloudStore]; + NSURL *localStoreURL = [self URLForLocalStore]; NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: self.contentName, NSPersistentStoreUbiquitousContentNameKey, [self URLForCloudContent], NSPersistentStoreUbiquitousContentURLKey, @@ -260,14 +369,13 @@ - (void)loadCloudStore { [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; // Now load the cloud store. If possible, first migrate the local store to it. - NSURL *localStoreURL = [self URLForLocalStore]; - UbiquityStoreManagerMigrationStrategy migrationStrategy = self.migrationStrategy; + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) - migrationStrategy = UbiquityStoreManagerMigrationStrategyNone; + migrationStrategy = UbiquityStoreMigrationStrategyNone; switch (migrationStrategy) { - case UbiquityStoreManagerMigrationStrategyCopyEntities: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyCopyEntities"]; + case UbiquityStoreMigrationStrategyCopyEntities: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; // Open local and cloud store. NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; @@ -276,7 +384,7 @@ - (void)loadCloudStore { options:localStoreOptions error:&error]; if (!localStore) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; break; } @@ -286,7 +394,7 @@ - (void)loadCloudStore { options:cloudStoreOptions error:&error]; if (!cloudStore) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } @@ -322,15 +430,15 @@ - (void)loadCloudStore { // Save migrated entities. if (!migrationFailure) - if (![cloudContext save:&error]) - migrationFailure = YES; + if (![cloudContext save:&error]) + migrationFailure = YES; // Handle failure by cleaning up the cloud store. if (migrationFailure) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseClearStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseClearStore context:cloudStore]; [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } @@ -340,13 +448,13 @@ - (void)loadCloudStore { configuration:nil URL:cloudStoreURL options:cloudStoreOptions error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; break; } - case UbiquityStoreManagerMigrationStrategyIOS: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyIOS"]; + case UbiquityStoreMigrationStrategyIOS: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; // Add the store to migrate. NSPersistentStore *localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType @@ -355,7 +463,7 @@ - (void)loadCloudStore { error:&error]; if (!localStore) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:localStoreURL]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; break; } @@ -364,19 +472,19 @@ - (void)loadCloudStore { options:cloudStoreOptions withType:NSSQLiteStoreType error:&error]) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; [self clearStore]; } break; } - case UbiquityStoreManagerMigrationStrategyManual: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreManagerMigrationStrategyManual"]; + case UbiquityStoreMigrationStrategyManual: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyManual"]; if (![self.delegate ubiquityStoreManager:self manuallyMigrateStore:localStoreURL withOptions:localStoreOptions - toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:cloudStoreURL.path]; + toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } @@ -386,12 +494,12 @@ - (void)loadCloudStore { configuration:nil URL:cloudStoreURL options:cloudStoreOptions error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } - case UbiquityStoreManagerMigrationStrategyNone: { + case UbiquityStoreMigrationStrategyNone: { [self log:@"Loading cloud store without local store migration."]; // Just add the store without first migrating to it. @@ -399,7 +507,7 @@ - (void)loadCloudStore { configuration:nil URL:cloudStoreURL options:cloudStoreOptions error:&error]) - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenCloudStore context:cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } } @@ -411,7 +519,7 @@ - (void)loadCloudStore { if (error) [userInfo setObject:error forKey:NSUnderlyingErrorKey]; [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] - cause:UbiquityStoreManagerErrorCauseMigrateLocalToCloudStore context:exception]; + cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = exception]; [self clearStore]; } @finally { @@ -422,13 +530,14 @@ - (void)loadCloudStore { [self observeStore]; } else { - // TODO: If this happens, the cloud store is desynced. Until we destroy it or fix it (seems impossible), iCloud will be unavailable to the user. + // If this happens, the cloud store is desynced. + // Until it is either fixed or destroyed, the cloud store will be unavailable to the user. [self resetTentativeStoreUUID]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u). Application will handle failure.", cause]; + [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; } else { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u). Will fall back to local store.", cause]; + [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; } } [self.persistentStoreCoordinator unlock]; @@ -438,8 +547,8 @@ - (void)loadCloudStore { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) - [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause wasCloud:YES]; + } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; else self.cloudEnabled = NO; }); @@ -454,7 +563,8 @@ - (void)loadLocalStore { // PSC is locked and busy with another operation. We can't use it. return; - UbiquityStoreManagerErrorCause cause; + UbiquityStoreErrorCause cause; + id context = nil; @try { [self clearStore]; @@ -469,16 +579,17 @@ - (void)loadLocalStore { // Make sure local store directory exists. if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForLocalStoreDirectory].path withIntermediateDirectories:YES attributes:nil error:&error]) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseCreateStorePath context:[self URLForCloudStoreDirectory].path]; + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForLocalStoreDirectory].path]; return; } // Add local store to PSC. + NSURL *localStoreURL = [self URLForLocalStore]; if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:[self URLForLocalStore] + configuration:nil URL:localStoreURL options:localStoreOptions error:&error]) { - [self error:error cause:cause = UbiquityStoreManagerErrorCauseOpenLocalStore context:[self URLForLocalStore]]; + [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; return; } } @@ -490,9 +601,9 @@ - (void)loadLocalStore { } else { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { - [self log:@"iCloud disabled but failed to load local store (cause:%u). Application will handle failure.", cause]; + [self log:@"iCloud disabled but failed to load local store (cause:%u, %@). Application will handle failure.", cause, context]; } else { - [self log:@"iCloud disabled but failed to load local store (cause:%u). No store available to application.", cause]; + [self log:@"iCloud disabled but failed to load local store (cause:%u, %@). No store available to application.", cause, context]; } } [self.persistentStoreCoordinator unlock]; @@ -502,8 +613,8 @@ - (void)loadLocalStore { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:NO]; [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) - [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause wasCloud:NO]; + } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:NO]; }); } } @@ -598,14 +709,14 @@ - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { NSError *error_ = nil; if (localOnly && [[NSFileManager defaultManager] isUbiquitousItemAtURL:newURL]) { if (![[NSFileManager defaultManager] evictUbiquitousItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + [self error:error_ cause:UbiquityStoreErrorCauseDeleteStore context:newURL.path]; } else { if (![[NSFileManager defaultManager] removeItemAtURL:newURL error:&error_]) - [self error:error_ cause:UbiquityStoreManagerErrorCauseDeleteStore context:newURL.path]; + [self error:error_ cause:UbiquityStoreErrorCauseDeleteStore context:newURL.path]; } }]; if (error) - [self error:error cause:UbiquityStoreManagerErrorCauseDeleteStore context:directoryURL.path]; + [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:directoryURL.path]; } - (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly { @@ -766,18 +877,26 @@ - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError - (void)applicationDidBecomeActive:(NSNotification *)note { - // Check for iCloud account changes. - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - id lastIdentityToken = [local objectForKey:CloudIdentityKey]; - id currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; - if (![lastIdentityToken isEqual:currentIdentityToken]) + // Check for iCloud identity changes (ie. user logs into another iCloud account). + if (![self.currentIdentityToken isEqual:[[NSFileManager defaultManager] ubiquityIdentityToken]]) [self cloudStoreChanged:nil]; } - (void)keyValueStoreChanged:(NSNotification *)note { - if ([(NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey] containsObject:StoreUUIDKey]) + NSArray *changedKeys = (NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; + if ([changedKeys containsObject:StoreUUIDKey]) + // The UUID of the active store changed. We need to switch to the newly activated store. [self cloudStoreChanged:nil]; + + if ([changedKeys containsObject:StoreAccessChoosingKey] || [changedKeys containsObject:StoreAccessTicketKey]) { + // Something changed with regards to exclusive access tickets. + if (self.cloudEnabled && !self.haveExclusiveAccess && self.desyncAvoidanceStrategy != UbiquityStoreDesyncAvoidanceStrategyNone) { + // Since cloud is enabled and we don't have exclusive access yet, let's see if we can claim it now. + [self log:@"Exclusive access tickets updated. Checking whether we've gained exclusive access."]; + [self loadStore]; + } + } } /** @@ -789,17 +908,14 @@ - (void)keyValueStoreChanged:(NSNotification *)note { - (void)cloudStoreChanged:(NSNotification *)note { // Update the identity token in case it changed. - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - id identityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; - [local setObject:identityToken forKey:CloudIdentityKey]; - [local synchronize]; + self.currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; // Don't reload the store when the local one is active. if (!self.cloudEnabled) return; // Reload the store. - [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, identityToken]; + [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, self.currentIdentityToken]; [self loadStore]; } @@ -813,11 +929,11 @@ - (void)mergeChanges:(NSNotification *)note { NSError *error = nil; if (![moc save:&error]) { - // TODO: If this happens, the cloud store is desynced. Until we destroy it or fix it (seems impossible), iCloud will be unavailable to the user. - [self error:error cause:UbiquityStoreManagerErrorCauseImportChanges context:note]; + [self error:error cause:UbiquityStoreErrorCauseImportChanges context:note]; // Try to reload the store to see if it's still viable. // If not, either the application will handle it or we'll fall back to the local store. + // TODO: Verify that this works reliably. [self loadStore]; } }]; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index fb2d90a..fc06830 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -163,7 +163,7 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsClou }); } -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreManagerErrorCause)cause wasCloud:(BOOL)wasCloudStore { +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore { dispatch_async(dispatch_get_main_queue(), ^{ [masterViewController.storeLoadingActivity stopAnimating]; From 39692c528944c2349c38056917147fa8780ce81b Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 7 Mar 2013 18:39:38 -0500 Subject: [PATCH 20/35] Change the way we store exclusive access tickets in the store to prevent overwriting another device's ticket changes. [FIXED] Overwriting of other device's ticket changes in KVS. [FIXED] Don't remove observation of KVS & application events when clearing the store. [UPDATED] When cloud store is unavailable, don't fall back to the local store; wait for it to become available. --- iCloudStoreManager/UbiquityStoreManager.m | 199 +++++++++++++++------- iCloudStoreManagerExample/AppDelegate.m | 4 + 2 files changed, 141 insertions(+), 62 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 7886c23..730ae91 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -22,9 +22,11 @@ NSString *const UbiquityManagedStoreExclusiveDeviceNameKey = @"UbiquityManagedStoreExclusiveDeviceNameKey"; NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. NSString *const DeviceUUIDKey = @"USMDeviceUUIDKey"; // local: The UUID of this device when checking exclusive access. -NSString *const StoreAccessChoosingKey = @"USMStoreAccessChoosingKey"; // cloud: device UUID -> whether that device is choosing a ticket. -NSString *const StoreAccessTicketKey = @"USMStoreAccessTicketKey"; // cloud: device UUID -> the ticket owned by that device. -NSString *const StoreAccessNameKey = @"USMStoreAccessNameKey"; // cloud: device UUID -> the name of that device. +NSString *const StoreAccessDevicesKey = @"USMStoreAccessDevicesKey"; // cloud: device UUIDs +NSString *const StoreAccessUUIDKey = @"USMStoreAccessUUIDKey"; // cloud: the UUID of the device. +NSString *const StoreAccessNameKey = @"USMStoreAccessNameKey"; // cloud: the name of the device. +NSString *const StoreAccessTicketKey = @"USMStoreAccessTicketKey"; // cloud: the ticket of the device. +NSString *const StoreAccessChoosingKey = @"USMStoreAccessChoosingKey"; // cloud: whether the device is choosing a ticket. NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. NSString *const CloudStoreDirectory = @"CloudStore.nosync"; NSString *const CloudLogsDirectory = @"CloudLogs"; @@ -57,7 +59,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb if (!(self = [super init])) return nil; - // Parameters + // Parameters. _delegate = delegate; _contentName = contentName == nil? @"UbiquityStore": contentName; _model = model == nil? [NSManagedObjectModel mergedModelFromBundles:nil]: model; @@ -69,7 +71,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb _containerIdentifier = containerIdentifier; _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; - // Private vars + // Private vars. _migrationStrategy = UbiquityStoreMigrationStrategyCopyEntities; _desyncAvoidanceStrategy = UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess; _persistentStorageQueue = [NSOperationQueue new]; @@ -78,6 +80,32 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb _presentedItemOperationQueue = [NSOperationQueue new]; _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass([self class])]; + // Observe application events. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) + name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification + object:[NSUbiquitousKeyValueStore defaultStore]]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cloudStoreChanged:) + name:NSUbiquityIdentityDidChangeNotification + object:nil]; +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; +#else + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) + name:NSApplicationDidBecomeActiveNotification + object:nil]; +#endif + [self loadStore]; return self; @@ -192,12 +220,12 @@ - (void)error:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id) - (void)clearStore { + [self log:@"Clearing %u stores, will load %@ store...", [self.persistentStoreCoordinator.persistentStores count], self.cloudEnabled?@"cloud": @"local"]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; // Remove store observers. [NSFileCoordinator removeFilePresenter:self]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; // Remove the store from the coordinator. NSError *error = nil; @@ -208,6 +236,7 @@ - (void)clearStore { if ([self.persistentStoreCoordinator.persistentStores count]) { // We couldn't remove all the stores, make a new PSC instead. [self.persistentStoreCoordinator unlock]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:self.persistentStoreCoordinator]; self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; [self.persistentStoreCoordinator lock]; } @@ -217,14 +246,27 @@ - (void)clearStore { // TODO: Can a device inadvertently stop using the store without relinquishing its ticket? Probably. Must handle. NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - NSString *ownUUID = [local objectForKey:DeviceUUIDKey]; - if (ownUUID) - [cloud setValue:@0 forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]]; + NSString *ownDeviceUUID = [local objectForKey:DeviceUUIDKey]; + if (ownDeviceUUID && ownDeviceUUID != (id)[NSNull null]) { + NSMutableDictionary *ownDevice = [[cloud dictionaryForKey:ownDeviceUUID] mutableCopy]; + id ownTicketObject = [ownDevice objectForKey:StoreAccessTicketKey]; + int ownTicket = [ownTicketObject respondsToSelector:@selector(intValue)] ? [ownTicketObject intValue] : 0; + if (ownTicket) { + [self log:@"Relinquishing our exclusive access ticket: %d", [ownTicketObject intValue]]; + + [ownDevice setObject:@0 forKey:StoreAccessTicketKey]; + [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; + [cloud synchronize]; + } + } + + self.haveExclusiveAccess = NO; } } - (void)loadStore { + [self log:@"Will load %@ store...", self.cloudEnabled? @"cloud": @"local"]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; @@ -262,51 +304,81 @@ - (void)loadCloudStore { // Try to obtain exclusive access for this device based on Lamport's bakery algorithm. NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + [self log:@"Cloud KVS: %@", [cloud dictionaryRepresentation]]; + + // Get the UUID and state dictionary of our device. + NSString *ownDeviceUUID = [local objectForKey:DeviceUUIDKey]; + if (!ownDeviceUUID) + [local setObject:ownDeviceUUID = [[NSUUID UUID] UUIDString] forKey:DeviceUUIDKey]; + NSMutableArray *deviceUUIDs = [[cloud arrayForKey:StoreAccessDevicesKey] mutableCopy]; + if (!deviceUUIDs) + [cloud setArray:deviceUUIDs = [NSMutableArray array] forKey:StoreAccessDevicesKey]; + if (![deviceUUIDs containsObject:ownDeviceUUID]) { + [deviceUUIDs addObject:ownDeviceUUID]; + [cloud setArray:deviceUUIDs forKey:StoreAccessDevicesKey]; + } + NSString *ownName = [[UIDevice currentDevice] name]; + NSMutableDictionary *ownDevice = [[cloud dictionaryForKey:ownDeviceUUID] mutableCopy]; + if (!ownDevice) + [cloud setDictionary:ownDevice = [NSMutableDictionary dictionaryWithObject:ownDeviceUUID forKey:StoreAccessUUIDKey] forKey:ownDeviceUUID]; + if (![[ownDevice objectForKey:StoreAccessNameKey] isEqual:ownName]) { + [ownDevice setObject:ownName forKey:StoreAccessNameKey]; + [cloud setObject:ownDevice forKey:ownDeviceUUID]; + } - // Get the UUID of our device. - NSString *ownUUID = [local objectForKey:DeviceUUIDKey]; - if (!ownUUID) - [local setObject:ownUUID = [[NSUUID UUID] UUIDString] forKey:DeviceUUIDKey]; - - // Check whether we have a ticket (and let other devices know our device name). - int ownTicket = [[cloud valueForKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]] intValue]; - [cloud setObject:[[UIDevice currentDevice] name] forKey:[NSString stringWithFormat:@"%@.%@", StoreAccessNameKey, ownUUID]]; + // Check whether we have a ticket and let other devices know our device name. + id ownTicketObject = [ownDevice objectForKey:StoreAccessTicketKey]; + int ownTicket = [ownTicketObject respondsToSelector:@selector(intValue)]? [ownTicketObject intValue]: 0; // If we don't have a ticket yet, choose one. if (!ownTicket) { - [cloud setValue:@YES - forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessChoosingKey, ownUUID]]; - [cloud synchronize]; - ownTicket = [[cloud valueForKeyPath:[NSString stringWithFormat:@"%@@max", StoreAccessTicketKey]] intValue] + 1; - [cloud setValue:@(ownTicket) - forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessTicketKey, ownUUID]]; - [cloud setValue:@NO - forKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessChoosingKey, ownUUID]]; + [ownDevice setObject:@YES forKey:StoreAccessChoosingKey]; + [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; [cloud synchronize]; - } + + int maxTicket = 0; + for (NSString *deviceUUID in deviceUUIDs) { + NSDictionary *device = [cloud dictionaryForKey:deviceUUID]; + if (!device || device == (id)[NSNull null]) + continue; + int deviceTicket = [[device objectForKey:StoreAccessTicketKey] intValue]; + maxTicket = MAX(maxTicket, deviceTicket); + } + + ownTicket = maxTicket + 1; + [ownDevice setObject:@NO forKey:StoreAccessChoosingKey]; + [ownDevice setObject:@(ownTicket) forKey:StoreAccessTicketKey]; + [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; + [self log:@"We don't have a ticket yet. Chose ticket: %d", ownTicket]; + } else + [self log:@"Using our existing ticket: %d", ownTicket]; // Check to see if our ticket gives us access to the store. - NSString *exclusiveDeviceUUID = nil; - NSDictionary *tickets = [cloud dictionaryForKey:StoreAccessTicketKey]; - NSDictionary *choosing = [cloud dictionaryForKey:StoreAccessChoosingKey]; - for (NSString *deviceUUID in [tickets allKeys]) - if (![deviceUUID isEqualToString:ownUUID]) { - if ([[choosing objectForKey:deviceUUID] boolValue]) { + [cloud synchronize]; + NSDictionary *exclusiveDevice = nil; + for (NSString *deviceUUID in deviceUUIDs) + if (![deviceUUID isEqualToString:ownDeviceUUID]) { + NSDictionary *device = [cloud dictionaryForKey:deviceUUID]; + BOOL deviceChoosing = !device || device == (id) [NSNull null] || [[device objectForKey:StoreAccessChoosingKey] boolValue]; + if (deviceChoosing) { // Another device is picking a ticket, we can't assert access yet. - exclusiveDeviceUUID = deviceUUID; + [self log:@"Device is choosing: %@", device]; + exclusiveDevice = device; break; } - int deviceTicket = [[tickets objectForKey:deviceUUID] intValue]; - if (deviceTicket && (deviceTicket < ownTicket || (deviceTicket == ownTicket && [deviceUUID compare:ownUUID] == NSOrderedAscending))) { + int deviceTicket = [[device objectForKey:StoreAccessTicketKey] intValue]; + if (deviceTicket && (deviceTicket < ownTicket || (deviceTicket == ownTicket && [deviceUUID compare:ownDeviceUUID] == NSOrderedAscending))) { // Another device has a ticket that comes before ours (or is the same as ours but their UUID sorts first). // We can't assert access until this device relinquishes their ticket. - exclusiveDeviceUUID = deviceUUID; + [self log:@"Device's ticket beats ours (%d): %@", ownTicket, device]; + exclusiveDevice = device; break; } } - if (!exclusiveDeviceUUID) + if (!exclusiveDevice) // No device is inhibiting exclusive access. Our ticket will assert our access to the other devices. self.haveExclusiveAccess = YES; @@ -317,11 +389,7 @@ - (void)loadCloudStore { case UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess: { // Fail loading the store. cause = UbiquityStoreErrorCauseNoExclusiveAccess; - context = @{ - UbiquityManagedStoreExclusiveDeviceUUIDKey : exclusiveDeviceUUID, - UbiquityManagedStoreExclusiveDeviceNameKey : - [cloud valueForKeyPath:[NSString stringWithFormat:@"%@.%@", StoreAccessNameKey, exclusiveDeviceUUID]] - }; + context = exclusiveDevice; return; } case UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess: { @@ -549,7 +617,7 @@ - (void)loadCloudStore { [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; - else + else if (cause != UbiquityStoreErrorCauseNoExclusiveAccess) self.cloudEnabled = NO; }); @@ -681,22 +749,6 @@ - (void)observeStore { name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:self.persistentStoreCoordinator]; } - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) - name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cloudStoreChanged:) - name:NSUbiquityIdentityDidChangeNotification - object:nil]; -#if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) - name:UIApplicationDidBecomeActiveNotification - object:nil]; -#else - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) - name:NSApplicationDidBecomeActiveNotification - object:nil]; -#endif } - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { @@ -736,7 +788,9 @@ - (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly { [self resetTentativeStoreUUID]; if (!localOnly) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + for (id key in [[cloud dictionaryRepresentation] allKeys]) + [cloud removeObjectForKey:key]; [cloud synchronize]; } @@ -764,7 +818,11 @@ - (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self resetTentativeStoreUUID]; if (!localOnly) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; [cloud removeObjectForKey:StoreUUIDKey]; + for (NSString *deviceUUID in [cloud arrayForKey:StoreAccessDevicesKey]) + [cloud removeObjectForKey:deviceUUID]; + [cloud removeObjectForKey:StoreAccessDevicesKey]; [cloud synchronize]; } @@ -882,14 +940,31 @@ - (void)applicationDidBecomeActive:(NSNotification *)note { [self cloudStoreChanged:nil]; } +- (void)applicationWillEnterForeground:(NSNotification *)note { + + [self loadStore]; +} + +- (void)applicationDidEnterBackground:(NSNotification *)note { + + [self clearStore]; +} + +- (void)applicationWillTerminate:(NSNotification *)note { + + [self clearStore]; +} + - (void)keyValueStoreChanged:(NSNotification *)note { NSArray *changedKeys = (NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; + [self log:@"KVS changed. Keys: %@", changedKeys]; if ([changedKeys containsObject:StoreUUIDKey]) // The UUID of the active store changed. We need to switch to the newly activated store. [self cloudStoreChanged:nil]; - if ([changedKeys containsObject:StoreAccessChoosingKey] || [changedKeys containsObject:StoreAccessTicketKey]) { + if ([changedKeys containsObject:StoreAccessDevicesKey] || + [changedKeys firstObjectCommonWithArray:[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:StoreAccessDevicesKey]]) { // Something changed with regards to exclusive access tickets. if (self.cloudEnabled && !self.haveExclusiveAccess && self.desyncAvoidanceStrategy != UbiquityStoreDesyncAvoidanceStrategyNone) { // Since cloud is enabled and we don't have exclusive access yet, let's see if we can claim it now. @@ -921,7 +996,7 @@ - (void)cloudStoreChanged:(NSNotification *)note { - (void)mergeChanges:(NSNotification *)note { - [self log:@"Cloud store updates:\n%@", note.userInfo]; + [self log:@"Importing ubiquity changes:\n%@", note.userInfo]; [self.persistentStorageQueue addOperationWithBlock:^{ NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; [moc performBlockAndWait:^{ diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index fc06830..60b2d2c 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -165,6 +165,10 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsClou - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore { + if (cause == UbiquityStoreErrorCauseNoExclusiveAccess) + // Just wait for the store to become available. + return; + dispatch_async(dispatch_get_main_queue(), ^{ [masterViewController.storeLoadingActivity stopAnimating]; }); From c58ea5aac3eef60b728da26e4b34c21d7b26a296 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 9 Mar 2013 11:01:13 -0500 Subject: [PATCH 21/35] Remove desync avoidance. [REMOVED] Desync avoidance strategies were unreliable and only worked while all devices are online. --- iCloudStoreManager/UbiquityStoreManager.h | 22 --- iCloudStoreManager/UbiquityStoreManager.m | 163 +--------------------- iCloudStoreManagerExample/AppDelegate.m | 4 - 3 files changed, 1 insertion(+), 188 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index badc9d9..9c62de6 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -23,14 +23,6 @@ extern NSString *const UbiquityManagedStoreDidChangeNotification; * The store managed by the ubiquity manager's coordinator imported changes from iCloud (eg. another device saved changes to iCloud). */ extern NSString *const UbiquityManagedStoreDidImportChangesNotification; -/** - * The key at which the UUID of the device that is inhibiting exclusive access to the store can be found in the context of UbiquityStoreErrorCauseNoExclusiveAccess. - */ -extern NSString *const UbiquityManagedStoreExclusiveDeviceUUIDKey; -/** - * The key at which the name of the device that is inhibiting exclusive access to the store can be found in the context of UbiquityStoreErrorCauseNoExclusiveAccess. - */ -extern NSString *const UbiquityManagedStoreExclusiveDeviceNameKey; typedef enum { UbiquityStoreErrorCauseNoAccount, // The user is not logged into iCloud on this device. There is no context. @@ -41,7 +33,6 @@ typedef enum { UbiquityStoreErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. context = the path of the store. UbiquityStoreErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. context = the path of the store or exception that caused the problem. UbiquityStoreErrorCauseImportChanges, // Error occurred while importing changes from the cloud into the application's context. context = the DidImportUbiquitousContentChanges notification. - UbiquityStoreErrorCauseNoExclusiveAccess // This device was unable to obtain exclusive access to the store. context = a dictionary with keys UbiquityManagedStoreExclusiveDeviceUUIDKey, UbiquityManagedStoreExclusiveDeviceNameKey. } UbiquityStoreErrorCause; typedef enum { @@ -51,13 +42,6 @@ typedef enum { UbiquityStoreMigrationStrategyNone, // Don't migrate, just create an empty destination store. } UbiquityStoreMigrationStrategy; -typedef enum { - UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess, // Avoid cloud desync by requesting exclusive access to the cloud store and failing to load the store if another device has it. Persistence will be unavailable while another device has exclusive access. - UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess, // Avoid cloud desync by requesting exclusive access to the cloud store and opening the store read-only if another device has it. Persistence will be read-only while another device has exclusive access. - UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal, // Avoid cloud desync by requesting exclusive access to the cloud store and migrating it to the local store if another device has it. Persistence will be read-write but changes won't be synced while another device has exclusive access. - UbiquityStoreDesyncAvoidanceStrategyNone, // Don't try to avoid cloud desync. If it happens, your application can try and cope with it from -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:. -} UbiquityStoreDesyncAvoidanceStrategy; - @class UbiquityStoreManager; @protocol UbiquityStoreManagerDelegate @@ -127,12 +111,6 @@ typedef enum { /** Determines what strategy to use when migrating from one store to another (eg. local -> cloud). Default is UbiquityStoreMigrationStrategyCopyEntities. */ @property (nonatomic, assign) UbiquityStoreMigrationStrategy migrationStrategy; -/** Determines what strategy to use to avoid causing the cloud store on different devices to get desynced. Default is UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess. - * - * Because of bugs in iOS' iCloud implementation, desyncs happen when two devices simultaneously mutate a relationship. - */ -@property (nonatomic, assign) UbiquityStoreDesyncAvoidanceStrategy desyncAvoidanceStrategy; - /** Indicates whether the iCloud store or the local store is in use. */ @property (nonatomic) BOOL cloudEnabled; diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 730ae91..179bf27 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -18,15 +18,7 @@ NSString *const UbiquityManagedStoreDidChangeNotification = @"UbiquityManagedStoreDidChangeNotification"; NSString *const UbiquityManagedStoreDidImportChangesNotification = @"UbiquityManagedStoreDidImportChangesNotification"; -NSString *const UbiquityManagedStoreExclusiveDeviceUUIDKey = @"UbiquityManagedStoreExclusiveDeviceUUIDKey"; -NSString *const UbiquityManagedStoreExclusiveDeviceNameKey = @"UbiquityManagedStoreExclusiveDeviceNameKey"; NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. -NSString *const DeviceUUIDKey = @"USMDeviceUUIDKey"; // local: The UUID of this device when checking exclusive access. -NSString *const StoreAccessDevicesKey = @"USMStoreAccessDevicesKey"; // cloud: device UUIDs -NSString *const StoreAccessUUIDKey = @"USMStoreAccessUUIDKey"; // cloud: the UUID of the device. -NSString *const StoreAccessNameKey = @"USMStoreAccessNameKey"; // cloud: the name of the device. -NSString *const StoreAccessTicketKey = @"USMStoreAccessTicketKey"; // cloud: the ticket of the device. -NSString *const StoreAccessChoosingKey = @"USMStoreAccessChoosingKey"; // cloud: whether the device is choosing a ticket. NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. NSString *const CloudStoreDirectory = @"CloudStore.nosync"; NSString *const CloudLogsDirectory = @"CloudLogs"; @@ -43,7 +35,6 @@ @interface UbiquityStoreManager () @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; -@property(nonatomic) BOOL haveExclusiveAccess; @property(nonatomic, strong) id currentIdentityToken; @end @@ -73,7 +64,6 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb // Private vars. _migrationStrategy = UbiquityStoreMigrationStrategyCopyEntities; - _desyncAvoidanceStrategy = UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess; _persistentStorageQueue = [NSOperationQueue new]; _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass([self class])]; _persistentStorageQueue.maxConcurrentOperationCount = 1; @@ -240,28 +230,6 @@ - (void)clearStore { self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; [self.persistentStoreCoordinator lock]; } - - // Store cleared. Relinquish exclusive access if we have it. - if (self.haveExclusiveAccess) { - // TODO: Can a device inadvertently stop using the store without relinquishing its ticket? Probably. Must handle. - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - NSString *ownDeviceUUID = [local objectForKey:DeviceUUIDKey]; - if (ownDeviceUUID && ownDeviceUUID != (id)[NSNull null]) { - NSMutableDictionary *ownDevice = [[cloud dictionaryForKey:ownDeviceUUID] mutableCopy]; - id ownTicketObject = [ownDevice objectForKey:StoreAccessTicketKey]; - int ownTicket = [ownTicketObject respondsToSelector:@selector(intValue)] ? [ownTicketObject intValue] : 0; - if (ownTicket) { - [self log:@"Relinquishing our exclusive access ticket: %d", [ownTicketObject intValue]]; - - [ownDevice setObject:@0 forKey:StoreAccessTicketKey]; - [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; - [cloud synchronize]; - } - } - - self.haveExclusiveAccess = NO; - } } - (void)loadStore { @@ -299,120 +267,6 @@ - (void)loadCloudStore { return; } - // Check for exclusive access. - if (self.desyncAvoidanceStrategy != UbiquityStoreDesyncAvoidanceStrategyNone) { - // Try to obtain exclusive access for this device based on Lamport's bakery algorithm. - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud synchronize]; - [self log:@"Cloud KVS: %@", [cloud dictionaryRepresentation]]; - - // Get the UUID and state dictionary of our device. - NSString *ownDeviceUUID = [local objectForKey:DeviceUUIDKey]; - if (!ownDeviceUUID) - [local setObject:ownDeviceUUID = [[NSUUID UUID] UUIDString] forKey:DeviceUUIDKey]; - NSMutableArray *deviceUUIDs = [[cloud arrayForKey:StoreAccessDevicesKey] mutableCopy]; - if (!deviceUUIDs) - [cloud setArray:deviceUUIDs = [NSMutableArray array] forKey:StoreAccessDevicesKey]; - if (![deviceUUIDs containsObject:ownDeviceUUID]) { - [deviceUUIDs addObject:ownDeviceUUID]; - [cloud setArray:deviceUUIDs forKey:StoreAccessDevicesKey]; - } - NSString *ownName = [[UIDevice currentDevice] name]; - NSMutableDictionary *ownDevice = [[cloud dictionaryForKey:ownDeviceUUID] mutableCopy]; - if (!ownDevice) - [cloud setDictionary:ownDevice = [NSMutableDictionary dictionaryWithObject:ownDeviceUUID forKey:StoreAccessUUIDKey] forKey:ownDeviceUUID]; - if (![[ownDevice objectForKey:StoreAccessNameKey] isEqual:ownName]) { - [ownDevice setObject:ownName forKey:StoreAccessNameKey]; - [cloud setObject:ownDevice forKey:ownDeviceUUID]; - } - - // Check whether we have a ticket and let other devices know our device name. - id ownTicketObject = [ownDevice objectForKey:StoreAccessTicketKey]; - int ownTicket = [ownTicketObject respondsToSelector:@selector(intValue)]? [ownTicketObject intValue]: 0; - - // If we don't have a ticket yet, choose one. - if (!ownTicket) { - [ownDevice setObject:@YES forKey:StoreAccessChoosingKey]; - [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; - [cloud synchronize]; - - int maxTicket = 0; - for (NSString *deviceUUID in deviceUUIDs) { - NSDictionary *device = [cloud dictionaryForKey:deviceUUID]; - if (!device || device == (id)[NSNull null]) - continue; - int deviceTicket = [[device objectForKey:StoreAccessTicketKey] intValue]; - maxTicket = MAX(maxTicket, deviceTicket); - } - - ownTicket = maxTicket + 1; - [ownDevice setObject:@NO forKey:StoreAccessChoosingKey]; - [ownDevice setObject:@(ownTicket) forKey:StoreAccessTicketKey]; - [cloud setDictionary:ownDevice forKey:ownDeviceUUID]; - [self log:@"We don't have a ticket yet. Chose ticket: %d", ownTicket]; - } else - [self log:@"Using our existing ticket: %d", ownTicket]; - - // Check to see if our ticket gives us access to the store. - [cloud synchronize]; - NSDictionary *exclusiveDevice = nil; - for (NSString *deviceUUID in deviceUUIDs) - if (![deviceUUID isEqualToString:ownDeviceUUID]) { - NSDictionary *device = [cloud dictionaryForKey:deviceUUID]; - BOOL deviceChoosing = !device || device == (id) [NSNull null] || [[device objectForKey:StoreAccessChoosingKey] boolValue]; - if (deviceChoosing) { - // Another device is picking a ticket, we can't assert access yet. - [self log:@"Device is choosing: %@", device]; - exclusiveDevice = device; - break; - } - - int deviceTicket = [[device objectForKey:StoreAccessTicketKey] intValue]; - if (deviceTicket && (deviceTicket < ownTicket || (deviceTicket == ownTicket && [deviceUUID compare:ownDeviceUUID] == NSOrderedAscending))) { - // Another device has a ticket that comes before ours (or is the same as ours but their UUID sorts first). - // We can't assert access until this device relinquishes their ticket. - [self log:@"Device's ticket beats ours (%d): %@", ownTicket, device]; - exclusiveDevice = device; - break; - } - } - - if (!exclusiveDevice) - // No device is inhibiting exclusive access. Our ticket will assert our access to the other devices. - self.haveExclusiveAccess = YES; - - else { - // Another device is inhibiting exclusive access for now. - // Let the strategy decide what to do in the mean time. - switch (self.desyncAvoidanceStrategy) { - case UbiquityStoreDesyncAvoidanceStrategyExclusiveAccess: { - // Fail loading the store. - cause = UbiquityStoreErrorCauseNoExclusiveAccess; - context = exclusiveDevice; - return; - } - case UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess: { - // Open the store read-only. - // TODO: Beware: this may cause trouble when importing ubiquity changes. - [NSException raise:NSGenericException - format:@"Strategy not yet implemented: UbiquityStoreDesyncAvoidanceStrategyExclusiveWriteAccess"]; - break; - } - case UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal: { - // Migrate/copy our cloud store file to the local store and open that one instead. - // TODO: Beware: if we set cloudEnabled = NO, we won't detect when we do gain exclusive access. - [NSException raise:NSGenericException - format:@"Strategy not yet implemented: UbiquityStoreDesyncAvoidanceStrategyExclusiveOrMigrateToLocal"]; - break; - } - case UbiquityStoreDesyncAvoidanceStrategyNone: - [NSException raise:NSInternalInconsistencyException - format:@"Strategy doesn't need exclusive access: UbiquityStoreDesyncAvoidanceStrategyNone"]; - } - } - } - // Create the path to the cloud store. if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path withIntermediateDirectories:YES attributes:nil error:&error]) @@ -617,7 +471,7 @@ - (void)loadCloudStore { [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; - else if (cause != UbiquityStoreErrorCauseNoExclusiveAccess) + else self.cloudEnabled = NO; }); @@ -818,11 +672,7 @@ - (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self resetTentativeStoreUUID]; if (!localOnly) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud synchronize]; [cloud removeObjectForKey:StoreUUIDKey]; - for (NSString *deviceUUID in [cloud arrayForKey:StoreAccessDevicesKey]) - [cloud removeObjectForKey:deviceUUID]; - [cloud removeObjectForKey:StoreAccessDevicesKey]; [cloud synchronize]; } @@ -958,20 +808,9 @@ - (void)applicationWillTerminate:(NSNotification *)note { - (void)keyValueStoreChanged:(NSNotification *)note { NSArray *changedKeys = (NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; - [self log:@"KVS changed. Keys: %@", changedKeys]; if ([changedKeys containsObject:StoreUUIDKey]) // The UUID of the active store changed. We need to switch to the newly activated store. [self cloudStoreChanged:nil]; - - if ([changedKeys containsObject:StoreAccessDevicesKey] || - [changedKeys firstObjectCommonWithArray:[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:StoreAccessDevicesKey]]) { - // Something changed with regards to exclusive access tickets. - if (self.cloudEnabled && !self.haveExclusiveAccess && self.desyncAvoidanceStrategy != UbiquityStoreDesyncAvoidanceStrategyNone) { - // Since cloud is enabled and we don't have exclusive access yet, let's see if we can claim it now. - [self log:@"Exclusive access tickets updated. Checking whether we've gained exclusive access."]; - [self loadStore]; - } - } } /** diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index 60b2d2c..fc06830 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -165,10 +165,6 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsClou - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore { - if (cause == UbiquityStoreErrorCauseNoExclusiveAccess) - // Just wait for the store to become available. - return; - dispatch_async(dispatch_get_main_queue(), ^{ [masterViewController.storeLoadingActivity stopAnimating]; }); From 558c4ca63c24a256aa666c4bc2678f9364d703a3 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 9 Mar 2013 20:58:26 -0500 Subject: [PATCH 22/35] More robustness. [IMPROVED] More robust handling of PSC locking and operation queues to avoid locking up under certain peculiar conditions (eg. deleting the container from another device or from the local device). [IMPROVED] Delete the stale store file when the store content is deleted to prevent corrupt cloud content from being created. [IMPROVED] Perform store deletion asynchronously. [IMPROVED] Notify the application immediately whenever the cloud store becomes unavailable so it can unset its MOCs and re-initialize its other persistence users (eg. fetchedResultsController). --- iCloudStoreManager/UbiquityStoreManager.h | 19 +- iCloudStoreManager/UbiquityStoreManager.m | 592 +++++++++++----------- iCloudStoreManagerExample/AppDelegate.m | 1 + 3 files changed, 311 insertions(+), 301 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 9c62de6..c87c45a 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -16,7 +16,9 @@ #import /** - * The store managed by the ubiquity manager's coordinator changed (eg. switched from iCloud to local). + * The store managed by the ubiquity manager's coordinator changed (eg. switching (no store) or switched to iCloud or local). + * + * This notification is posted after the -ubiquityStoreManager:willLoadStoreIsCloud: or -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: message was posted to the delegate. */ extern NSString *const UbiquityManagedStoreDidChangeNotification; /** @@ -65,7 +67,9 @@ typedef enum { /** Triggered when the store manager loads a persistence store. * * This is where you'll init/update your application's persistence layer. - * You should probably create your main managed object context here. Note the coordinator could change during the application's lifetime. + * You should probably create your main managed object context here. + * + * Note the coordinator could change during the application's lifetime (you'll get a new -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: if this happens). */ @required - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; @@ -73,12 +77,7 @@ typedef enum { /** Triggered when the store manager fails to loads a persistence store. * * Useful to decide what to do to make a store available to the application. - * You should probably unset your managed object contexts here to prevent exceptions in your applications (the coordinator has no more store). * If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. You can implement this simply with `manager.cloudEnabled = NO;`. - * - * IMPORTANT: When this method is triggered, the store is likely irreparably broken. - * Unless your application has a way to recover, you should probably delete the store in question (cloud/local). - * Until you do, the user will remain unable to use that store. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore; @@ -132,19 +131,19 @@ typedef enum { * * Unless you intend to delete more than just the active cloud store, you should probably use -deleteCloudStoreLocalOnly: instead. */ -- (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly; +- (void)deleteCloudContainerLocalOnly:(BOOL)localOnly; /** * This will delete the iCloud store. * * @param localOnly If YES, the iCloud transaction logs will be redownloaded and the store rebuilt. If NO, the store will be permanently lost and a new one will be created by migrating the device's local store. */ -- (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly; +- (void)deleteCloudStoreLocalOnly:(BOOL)localOnly; /** * This will delete the local store. There is no recovery. */ -- (BOOL)deleteLocalStore; +- (void)deleteLocalStore; /** * Determine whether it's safe to seed the cloud store with a local store. diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 179bf27..c8516d2 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -33,13 +33,14 @@ @interface UbiquityStoreManager () @property (nonatomic, readonly) NSString *storeUUID; @property (nonatomic, strong) NSString *tentativeStoreUUID; @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; -@property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; +@property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; @property(nonatomic, strong) id currentIdentityToken; @end @implementation UbiquityStoreManager { + NSPersistentStoreCoordinator *_persistentStoreCoordinator; NSOperationQueue *_presentedItemOperationQueue; } @@ -95,6 +96,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb name:NSApplicationDidBecomeActiveNotification object:nil]; #endif + [NSFileCoordinator addFilePresenter:self]; [self loadStore]; @@ -103,6 +105,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb - (void)dealloc { + [NSFileCoordinator removeFilePresenter:self]; [self.persistentStoreCoordinator tryLock]; [self clearStore]; [self.persistentStoreCoordinator unlock]; @@ -208,14 +211,41 @@ - (void)error:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id) #pragma mark - Store Management +- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { + + if (!_persistentStoreCoordinator) { + _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) + name:NSPersistentStoreDidImportUbiquitousContentChangesNotification + object:_persistentStoreCoordinator]; + } + + return _persistentStoreCoordinator; +} + +- (void)resetPersistentStoreCoordinator { + + BOOL wasLocked = NO; + if (_persistentStoreCoordinator) { + wasLocked = ![_persistentStoreCoordinator tryLock]; + [_persistentStoreCoordinator unlock]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:_persistentStoreCoordinator]; + } + + if (wasLocked) + [self.persistentStoreCoordinator lock]; +} + - (void)clearStore { - [self log:@"Clearing %u stores, will load %@ store...", [self.persistentStoreCoordinator.persistentStores count], self.cloudEnabled?@"cloud": @"local"]; + [self log:@"Clearing stores, will load %@ store...", self.cloudEnabled?@"cloud": @"local"]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; - // Remove store observers. - [NSFileCoordinator removeFilePresenter:self]; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + }); // Remove the store from the coordinator. NSError *error = nil; @@ -223,268 +253,265 @@ - (void)clearStore { if (![self.persistentStoreCoordinator removePersistentStore:store error:&error]) [self error:error cause:UbiquityStoreErrorCauseClearStore context:store]; - if ([self.persistentStoreCoordinator.persistentStores count]) { + if ([self.persistentStoreCoordinator.persistentStores count]) // We couldn't remove all the stores, make a new PSC instead. - [self.persistentStoreCoordinator unlock]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:self.persistentStoreCoordinator]; - self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - [self.persistentStoreCoordinator lock]; - } + [self resetPersistentStoreCoordinator]; } - (void)loadStore { - [self log:@"Will load %@ store...", self.cloudEnabled? @"cloud": @"local"]; + [self log:@"Will load %@ store...", self.cloudEnabled ? @"cloud" : @"local"]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; - if (!self.persistentStoreCoordinator) - self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - - if (self.cloudEnabled) - [self loadCloudStore]; - else - [self loadLocalStore]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [self.persistentStoreCoordinator lock]; + @try { + if (self.cloudEnabled) + [self loadCloudStore]; + else + [self loadLocalStore]; + } @finally { + [self.persistentStoreCoordinator unlock]; + } + }]; } - (void)loadCloudStore { - // Load iCloud store asynchronously (init of iCloud may take some time). - [self.persistentStorageQueue addOperationWithBlock:^{ - if (![self.persistentStoreCoordinator tryLock]) - // PSC is locked and busy with another operation. We can't use it. + NSError *error = nil; + UbiquityStoreErrorCause cause; + id context = nil; + @try { + [self clearStore]; + + // Check if the user is logged into iCloud on the device. + if (![self URLForCloudContainer]) { + cause = UbiquityStoreErrorCauseNoAccount; return; + } - NSError *error = nil; - UbiquityStoreErrorCause cause; - id context = nil; - @try { - [self clearStore]; + // Create the path to the cloud store. + NSURL *cloudStoreURL = [self URLForCloudStore]; + NSURL *localStoreURL = [self URLForLocalStore]; + NSURL *cloudStoreContentURL = [self URLForCloudContent]; + NSURL *cloudStoreDirectoryURL = [self URLForCloudStoreDirectory]; + BOOL storeExists = [[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]; + BOOL storeContentExists = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:cloudStoreContentURL error:nil]; + if (storeExists && !storeContentExists) { + // We have a cloud store but no cloud content. The cloud content was deleted: + // The existing store cannot sync anymore and needs to be recreated. + if (![[NSFileManager defaultManager] removeItemAtURL:cloudStoreURL error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseDeleteStore context:context = cloudStoreURL.path]; + } + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreDirectoryURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreDirectoryURL.path]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreContentURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreContentURL.path]; + + // Add cloud store to PSC. + NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + self.contentName, NSPersistentStoreUbiquitousContentNameKey, + cloudStoreContentURL, NSPersistentStoreUbiquitousContentURLKey, + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSReadOnlyPersistentStoreOption, + nil]; + [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - // Check if the user is logged into iCloud on the device. - if (![self URLForCloudContainer]) { - cause = UbiquityStoreErrorCauseNoAccount; - return; - } + // Now load the cloud store. If possible, first migrate the local store to it. + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; + if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) + migrationStrategy = UbiquityStoreMigrationStrategyNone; + + switch (migrationStrategy) { + case UbiquityStoreMigrationStrategyCopyEntities: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; + + // Open local and cloud store. + NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + NSPersistentStore *localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:localStoreURL + options:localStoreOptions + error:&error]; + if (!localStore) { + [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; + break; + } - // Create the path to the cloud store. - if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudStoreDirectory].path - withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForCloudStoreDirectory].path]; - if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForCloudContent].path - withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForCloudContent].path]; - - // Add cloud store to PSC. - NSURL *cloudStoreURL = [self URLForCloudStore]; - NSURL *localStoreURL = [self URLForLocalStore]; - NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - self.contentName, NSPersistentStoreUbiquitousContentNameKey, - [self URLForCloudContent], NSPersistentStoreUbiquitousContentURLKey, - @YES, NSMigratePersistentStoresAutomaticallyOption, - @YES, NSInferMappingModelAutomaticallyOption, - nil]; - NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - @YES, NSReadOnlyPersistentStoreOption, - nil]; - [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - - // Now load the cloud store. If possible, first migrate the local store to it. - UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; - if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) - migrationStrategy = UbiquityStoreMigrationStrategyNone; - - switch (migrationStrategy) { - case UbiquityStoreMigrationStrategyCopyEntities: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; - - // Open local and cloud store. - NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - NSPersistentStore *localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:localStoreURL - options:localStoreOptions - error:&error]; - if (!localStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; - break; - } + NSPersistentStoreCoordinator *cloudCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + NSPersistentStore *cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]; + if (!cloudStore) { + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; + break; + } - NSPersistentStoreCoordinator *cloudCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - NSPersistentStore *cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]; - if (!cloudStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; + // Set up contexts for them. + NSManagedObjectContext *localContext = [NSManagedObjectContext new]; + NSManagedObjectContext *cloudContext = [NSManagedObjectContext new]; + localContext.persistentStoreCoordinator = localCoordinator; + cloudContext.persistentStoreCoordinator = cloudCoordinator; + + // Copy metadata. + NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:localStore] mutableCopy]; + [metadata addEntriesFromDictionary:[cloudCoordinator metadataForPersistentStore:cloudStore]]; + [cloudCoordinator setMetadata:metadata forPersistentStore:cloudStore]; + + // Migrate entities. + BOOL migrationFailure = NO; + NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; + for (NSEntityDescription *entity in self.model.entities) { + NSFetchRequest *fetch = [NSFetchRequest new]; + fetch.entity = entity; + fetch.fetchBatchSize = 500; + fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; + + NSArray *localObjects = [localContext executeFetchRequest:fetch error:&error]; + if (!localObjects) { + migrationFailure = YES; break; } - // Set up contexts for them. - NSManagedObjectContext *localContext = [NSManagedObjectContext new]; - NSManagedObjectContext *cloudContext = [NSManagedObjectContext new]; - localContext.persistentStoreCoordinator = localCoordinator; - cloudContext.persistentStoreCoordinator = cloudCoordinator; - - // Copy metadata. - NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:localStore] mutableCopy]; - [metadata addEntriesFromDictionary:[cloudCoordinator metadataForPersistentStore:cloudStore]]; - [cloudCoordinator setMetadata:metadata forPersistentStore:cloudStore]; - - // Migrate entities. - BOOL migrationFailure = NO; - NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; - for (NSEntityDescription *entity in self.model.entities) { - NSFetchRequest *fetch = [NSFetchRequest new]; - fetch.entity = entity; - fetch.fetchBatchSize = 500; - fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; - - NSArray *localObjects = [localContext executeFetchRequest:fetch error:&error]; - if (!localObjects) { - migrationFailure = YES; - break; - } - - for (NSManagedObject *localObject in localObjects) - [self copyMigrateObject:localObject toContext:cloudContext usingMigrationCache:migratedIDsBySourceID]; - } - - // Save migrated entities. - if (!migrationFailure) - if (![cloudContext save:&error]) - migrationFailure = YES; + for (NSManagedObject *localObject in localObjects) + [self copyMigrateObject:localObject toContext:cloudContext usingMigrationCache:migratedIDsBySourceID]; + } - // Handle failure by cleaning up the cloud store. - if (migrationFailure) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + // Save migrated entities. + if (!migrationFailure && ![cloudContext save:&error]) + migrationFailure = YES; - if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseClearStore context:cloudStore]; - [self removeItemAtURL:cloudStoreURL localOnly:NO]; - break; - } - - // Add the store now that migration is finished. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + // Handle failure by cleaning up the cloud store. + if (migrationFailure) { + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseClearStore context:cloudStore]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } - case UbiquityStoreMigrationStrategyIOS: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; + // Add the store now that migration is finished. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; - // Add the store to migrate. - NSPersistentStore *localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:localStoreURL - options:localStoreOptions - error:&error]; + break; + } - if (!localStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; - break; - } + case UbiquityStoreMigrationStrategyIOS: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; - if (![self.persistentStoreCoordinator migratePersistentStore:localStore - toURL:cloudStoreURL - options:cloudStoreOptions - withType:NSSQLiteStoreType - error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; - [self clearStore]; - } + // Add the store to migrate. + NSPersistentStore *localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:localStoreURL + options:localStoreOptions + error:&error]; + + if (!localStore) { + [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; break; } - case UbiquityStoreMigrationStrategyManual: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyManual"]; - - if (![self.delegate ubiquityStoreManager:self - manuallyMigrateStore:localStoreURL withOptions:localStoreOptions - toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; - [self removeItemAtURL:cloudStoreURL localOnly:NO]; - break; - } + if (![self.persistentStoreCoordinator migratePersistentStore:localStore + toURL:cloudStoreURL + options:cloudStoreOptions + withType:NSSQLiteStoreType + error:&error]) { + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + [self clearStore]; + } + break; + } - // Add the store now that migration is finished. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; + case UbiquityStoreMigrationStrategyManual: { + [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyManual"]; + if (![self.delegate ubiquityStoreManager:self + manuallyMigrateStore:localStoreURL withOptions:localStoreOptions + toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } - case UbiquityStoreMigrationStrategyNone: { - [self log:@"Loading cloud store without local store migration."]; + // Add the store now that migration is finished. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - // Just add the store without first migrating to it. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - break; - } + break; + } + + case UbiquityStoreMigrationStrategyNone: { + [self log:@"Loading cloud store without local store migration."]; + + // Just add the store without first migrating to it. + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:cloudStoreURL + options:cloudStoreOptions + error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; + break; } } - @catch (id exception) { - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; - if (exception) - [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; - if (error) - [userInfo setObject:error forKey:NSUnderlyingErrorKey]; - [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] - cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = exception]; - [self clearStore]; + } + @catch (id exception) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; + if (exception) + [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; + if (error) + [userInfo setObject:error forKey:NSUnderlyingErrorKey]; + [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] + cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = exception]; + [self clearStore]; + } + @finally { + BOOL cloudWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; + if (cloudWasEnabled) { + [self confirmTentativeStoreUUID]; + [self log:@"iCloud enabled (UUID:%@) and successfully loaded cloud store.", self.storeUUID]; } - @finally { - BOOL cloudWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; - if (cloudWasEnabled) { - [self confirmTentativeStoreUUID]; - [self log:@"iCloud enabled (UUID:%@) and successfully loaded cloud store.", self.storeUUID]; - [self observeStore]; - } - else { - // If this happens, the cloud store is desynced. - // Until it is either fixed or destroyed, the cloud store will be unavailable to the user. - [self resetTentativeStoreUUID]; - - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; - } else { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; - } + else { + // If this happens, the cloud store is desynced. + // Until it is either fixed or destroyed, the cloud store will be unavailable to the user. + [self resetTentativeStoreUUID]; + + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { + [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; + } else { + [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; } - [self.persistentStoreCoordinator unlock]; + } - dispatch_async(dispatch_get_main_queue(), ^{ - if (cloudWasEnabled) { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) - [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) - [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; - else - self.cloudEnabled = NO; - }); + dispatch_async(dispatch_get_main_queue(), ^{ + if (cloudWasEnabled) { + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; - } - }]; + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; + else + self.cloudEnabled = NO; + }); + } } - (void)loadLocalStore { - if (![self.persistentStoreCoordinator tryLock]) - // PSC is locked and busy with another operation. We can't use it. - return; - UbiquityStoreErrorCause cause; id context = nil; @try { @@ -519,7 +546,6 @@ - (void)loadLocalStore { BOOL localWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; if (localWasEnabled) { [self log:@"iCloud disabled and successfully loaded local store."]; - [self observeStore]; } else { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { @@ -528,13 +554,14 @@ - (void)loadLocalStore { [self log:@"iCloud disabled but failed to load local store (cause:%u, %@). No store available to application.", cause, context]; } } - [self.persistentStoreCoordinator unlock]; dispatch_async(dispatch_get_main_queue(), ^{ if (localWasEnabled) { if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:NO]; - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; + + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:NO]; }); @@ -595,16 +622,6 @@ - (BOOL)cloudSafeForSeeding { return YES; } -- (void)observeStore { - - if (self.cloudEnabled) { - [NSFileCoordinator addFilePresenter:self]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) - name:NSPersistentStoreDidImportUbiquitousContentChangesNotification - object:self.persistentStoreCoordinator]; - } -} - - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { NSError *error = nil; @@ -625,80 +642,65 @@ - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:directoryURL.path]; } -- (BOOL)deleteCloudContainerLocalOnly:(BOOL)localOnly { +- (void)deleteCloudContainerLocalOnly:(BOOL)localOnly { - if (![self.persistentStoreCoordinator tryLock]) { - [self log:@"Cannot delete the cloud container: Manager is locked."]; - return NO; - } - [self log:@"Will delete the cloud container %@.", localOnly? @"on this device": @"on this device and in the cloud"]; - - [self clearStore]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the cloud container %@.", localOnly ? @"on this device" : @"on this device and in the cloud"]; - // Delete the whole cloud container. - [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; - - // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; - if (!localOnly) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud synchronize]; - for (id key in [[cloud dictionaryRepresentation] allKeys]) - [cloud removeObjectForKey:key]; - [cloud synchronize]; - } + [self clearStore]; - [self.persistentStoreCoordinator unlock]; - [self loadStore]; + // Delete the whole cloud container. + [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; + + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + for (id key in [[cloud dictionaryRepresentation] allKeys]) + [cloud removeObjectForKey:key]; + [cloud synchronize]; + } - return YES; + [self loadStore]; + }]; } -- (BOOL)deleteCloudStoreLocalOnly:(BOOL)localOnly { - - if (![self.persistentStoreCoordinator tryLock]) { - [self log:@"Cannot delete the cloud store: Manager is locked."]; - return NO; - } - [self log:@"Will delete the cloud store (UUID:%@) %@.", self.storeUUID, localOnly ? @"on this device" : @"on this device and in the cloud"]; +- (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { - [self clearStore]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the cloud store (UUID:%@) %@.", self.storeUUID, localOnly ? @"on this device" : @"on this device and in the cloud"]; - // Clean up any cloud stores and transaction logs. - [self removeItemAtURL:[self URLForCloudStoreDirectory] localOnly:localOnly]; - [self removeItemAtURL:[self URLForCloudContentDirectory] localOnly:localOnly]; + [self clearStore]; - // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; - if (!localOnly) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - } + // Clean up any cloud stores and transaction logs. + [self removeItemAtURL:[self URLForCloudStoreDirectory] localOnly:localOnly]; + [self removeItemAtURL:[self URLForCloudContentDirectory] localOnly:localOnly]; - [self.persistentStoreCoordinator unlock]; - [self loadStore]; + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + } - return YES; + [self loadStore]; + }]; } -- (BOOL)deleteLocalStore { +- (void)deleteLocalStore { - if (![self.persistentStoreCoordinator tryLock]) { - [self log:@"Cannot delete the local store: Manager is locked."]; - return NO; - } - [self log:@"Will delete the local store."]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will delete the local store."]; - [self clearStore]; + [self clearStore]; - // Remove just the local store. - [self removeItemAtURL:[self URLForLocalStoreDirectory] localOnly:YES]; + // Remove just the local store. + [self removeItemAtURL:[self URLForLocalStoreDirectory] localOnly:YES]; - [self.persistentStoreCoordinator unlock]; - [self loadStore]; - - return YES; + [self loadStore]; + }]; } #pragma mark - Properties @@ -763,7 +765,7 @@ - (void)resetTentativeStoreUUID { - (NSURL *)presentedItemURL { if (self.cloudEnabled) - return [self URLForCloudStore]; + return [self URLForCloudContent]; return [self URLForLocalStore]; } @@ -775,9 +777,17 @@ -(NSOperationQueue *)presentedItemOperationQueue { - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { - // Active store file was deleted. - [self cloudStoreChanged:nil]; completionHandler(nil); + + // Reload the PSC's store. + [self clearStore]; + + NSError *error; + if (self.cloudEnabled) + if (![[NSFileManager defaultManager] removeItemAtURL:[self URLForCloudStore] error:&error]) + [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:[self URLForCloudStore].path]; + + [self loadStore]; } diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index fc06830..6fd6e4b 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -157,6 +157,7 @@ - (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(Ubi - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore { + __managedObjectContext = nil; dispatch_async(dispatch_get_main_queue(), ^{ [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; [masterViewController.storeLoadingActivity startAnimating]; From cff0a2eb06478bbbcc75bbac60bb1305912799f0 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 9 Mar 2013 21:47:27 -0500 Subject: [PATCH 23/35] A few doc updates. [UPDATED] A few more details about default implementations. --- iCloudStoreManager/UbiquityStoreManager.h | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index c87c45a..3bf48dc 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -57,7 +57,7 @@ typedef enum { /** Triggered when the store manager begins loading a persistence store. * - * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:, the application should not be using the persistence coordinator. + * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:, the application should not be using the persistence coordinator. * You should probably unset your managed object contexts here to prevent exceptions/hangs in your applications (the coordinator is locked and its store removed). * Also useful for indicating in your user interface that the store is loading. */ @@ -77,21 +77,31 @@ typedef enum { /** Triggered when the store manager fails to loads a persistence store. * * Useful to decide what to do to make a store available to the application. - * If you don't implement this, the default behaviour is to disable cloud when loading the cloud store fails and do nothing when loading the local store fails. You can implement this simply with `manager.cloudEnabled = NO;`. + * + * If you don't implement this method, the manager will disable the cloud store and fall back to the local store when loading the cloud store fails. It's the equivalent to implementing this method with `manager.cloudEnabled = NO;`. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore; -/** Triggered when the store manager encounters an error. Mainly useful to handle error conditions in whatever way you see fit. */ +/** Triggered when the store manager encounters an error. Mainly useful to handle error conditions/logging in whatever way you see fit. + * + * If you don't implement this method, the manager will instead detail the error in a few log statements. + */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id)context; -/** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. */ +/** Triggered whenever the store manager has information to share about its operation. Mainly useful to plug in your own logger. + * + * If you don't implement this method, the manager will just log the message using NSLog. + */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message; /** Triggered when the store manager needs to perform a manual store migration. + * + * Implementing this method is required if you set -migrationStrategy to UbiquityStoreMigrationStrategyManual. + * * @param error Write out an error object here when the migration fails. * @return YES when the migration was successful and the new store may be loaded. NO to error out and not load the new store (new store will be cleaned up if it exists). */ From 78ff8861c9cc9973ca01f6840cacc0b7f3a8786a Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 15 Mar 2013 00:01:10 -0400 Subject: [PATCH 24/35] Detect cloud content corruption and inform the delegate. [ADDED] A method of detecting cloud content corruption. [ADDED] When the cloud content is corrupted, all devices unload the cloud store and inform the delegate. The cloud store will no longer be available until the corruption is cleared. The documentation specifies approaches the application can take to clear the corruption (WIP). --- .gitmodules | 3 + .../NSManagedObject+UbiquityStoreManager.h | 17 ++ .../NSManagedObject+UbiquityStoreManager.m | 29 ++ iCloudStoreManager/UbiquityStoreManager.h | 115 +++++++- iCloudStoreManager/UbiquityStoreManager.m | 264 +++++++++++++----- .../project.pbxproj | 20 ++ jrswizzle | 1 + 7 files changed, 362 insertions(+), 87 deletions(-) create mode 100644 .gitmodules create mode 100644 iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h create mode 100644 iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m create mode 160000 jrswizzle diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1e11905 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "jrswizzle"] + path = jrswizzle + url = git://github.com/jonmarimba/jrswizzle.git diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h new file mode 100644 index 0000000..7e31f6d --- /dev/null +++ b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h @@ -0,0 +1,17 @@ +// +// Created by lhunath on 2013-03-13. +// +// To change the template use AppCode | Preferences | File Templates. +// + + +#import + +NSString *const UbiquityManagedStoreDidDetectCorruptionNotification = @"UbiquityManagedStoreDidDetectCorruptionNotification"; +NSString *const StoreCorruptedKey = @"USMStoreCorruptedKey"; // cloud: Set to YES when a cloud content corruption has been detected. + +@interface NSError (UbiquityStoreManager) + +- (id)init_USM_WithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict; + +@end diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m new file mode 100644 index 0000000..ff34aab --- /dev/null +++ b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m @@ -0,0 +1,29 @@ +// +// Created by lhunath on 2013-03-13. +// +// To change the template use AppCode | Preferences | File Templates. +// + + +#import "NSManagedObject+UbiquityStoreManager.h" + + +@implementation NSError (UbiquityStoreManager) + +- (id)init_USM_WithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDictionary *)dict { + + self = [self init_USM_WithDomain:domain code:code userInfo:dict]; + if ([domain isEqualToString:NSCocoaErrorDomain] && code == 134302) { + NSLog(@"Detected iCloud transaction log import failure: %@", self); + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setValue:@YES forKeyPath:StoreCorruptedKey]; + [cloud synchronize]; + + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidDetectCorruptionNotification + object:self]; + } + + return self; +} + +@end diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 3bf48dc..abe0bfa 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -2,14 +2,33 @@ // UbiquityStoreManager.h // UbiquityStoreManager // -// Created by Aleksey Novicov on 3/27/12. -// Copyright (c) 2012 Yodel Code LLC. All rights reserved. +// UbiquityStoreManager is a controller for your Core Data persistence layer. +// It provides you with an NSPersistentStoreCoordinator and handles the stores for you. +// It encapsulates everything required to make Core Data integration with iCloud work as reliably as possible. // -// UbiquityStoreManager manages the transfer of your SQL CoreData store from your local -// application sandbox to iCloud. Even though it is not reinforced, UbiquityStoreManager -// is expected to be used as a singleton. +// Aside from this, it features the following functionality: // -// NSUbiquitousKeyValueStore is the mechanism used to discover which iCloud store to use. +// - Ability to switch between a separate cloud-synced and local store (an iCloud toggle). +// - Automatically migrates local data to iCloud when the user has no iCloud store yet. +// - Handles all iCloud related events such as: +// - Account changes +// - External deletion of the cloud data +// - External deletion of the local store +// - Importing of ubiquitous changes from other devices +// - Recovering from exceptional events such as corrupted transaction logs +// - Some maintenance functionality: +// - Ability to rebuild the cloud store from transaction logs +// - Ability to delete the cloud store (allowing it to be recreated from the local store) +// - Ability to nuke the entire cloud container +// +// Known issues: +// - Sometimes Apple's iCloud implementation hangs itself coordinating access for importing ubiquitous changes. +// - Reloading the store with -loadStore can sometimes cause these changes to get imported. +// - If not, the app needs to be restarted. +// - Sometimes Apple's iCloud implementation will write corrupting transaction logs to the cloud container. +// - As a result, all other devices will fail to import any future changes to the store. +// - The only remedy is to recreate the store. +// - TODO: This manager allows the cloud store to be recreated and seeded by the old cloud store. // #import @@ -27,6 +46,7 @@ extern NSString *const UbiquityManagedStoreDidChangeNotification; extern NSString *const UbiquityManagedStoreDidImportChangesNotification; typedef enum { + UbiquityStoreErrorCauseNoError, // Nothing went wrong. There is no context. UbiquityStoreErrorCauseNoAccount, // The user is not logged into iCloud on this device. There is no context. UbiquityStoreErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. context = the path of the store. UbiquityStoreErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. context = the path of the store. @@ -48,11 +68,18 @@ typedef enum { @protocol UbiquityStoreManagerDelegate -/** The application should provide a managed object context to use for importing cloud changes. +/** When cloud changes are detected, the manager can merge these changes into your managed object context. + * + * If you don't implement this method or return nil, the manager will commit the changes to the store + * (using NSMergeByPropertyObjectTrumpMergePolicy) but your application may not become aware of them. + * + * If you do implement this method, the changes will be merged into your managed object context + * and the context will be saved afterwards. * - * After importing the changes to the context, the context will be saved. + * Regardless of whether this method is implemented or not, a UbiquityManagedStoreDidImportChangesNotification will be + * posted after the changes are successfully imported into the store. */ -@required +@optional - (NSManagedObjectContext *)managedObjectContextForUbiquityChangesInManager:(UbiquityStoreManager *)manager; /** Triggered when the store manager begins loading a persistence store. @@ -60,6 +87,9 @@ typedef enum { * Between this and an invocation of -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: or -ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:, the application should not be using the persistence coordinator. * You should probably unset your managed object contexts here to prevent exceptions/hangs in your applications (the coordinator is locked and its store removed). * Also useful for indicating in your user interface that the store is loading. + * + * @param isCloudStore YES if the cloud store will be loaded. + * NO if the local store will be loaded. */ @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore; @@ -70,18 +100,67 @@ typedef enum { * You should probably create your main managed object context here. * * Note the coordinator could change during the application's lifetime (you'll get a new -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: if this happens). + * + * @param isCloudStore YES if the cloud store was just loaded. + * NO if the local store was just loaded. */ @required - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; +/** Triggered when the store manager has detected that the cloud content has failed to import on one of the devices. + * + * The result is that the cloud store on this device is no longer guaranteed to be the same as the cloud store on + * other devices. Moreover, there is no more guarantee that changes made to the cloud store will sync to other devices. + * iCloud sync for the cloud store is therefore effectively broken. + * + * When this happens, there is only one recovery: The cloud store must be recreated. + * + * The most likely cause of this is an Apple bug with regards to synchronizing Core Data relationships using + * transaction logs. When two devices simultaneously modify a relationship, the resulting transaction logs can cause + * an irreparable conflict. + * + * The manager protects the user from committing more data into the corrupt cloud content container. + * If you implement this method, it will be invoked on every device that attempts to use the cloud store until + * the cloud store is rebuilt. After invoking this method, if the cloud store is currently enabled, it will be + * unloaded and the store coordinator will have no store available. + * If you don't implement this method, the manager will switch to the local store and the cloud store will remain + * unavailable. + * + * When you receive this method, there are a few things you can do to handle the situation: + * - Switch to the local store (manager.cloudEnabled = NO). + * NOTE: The cloud data and cloud syncing will be unavailable. + * - Keep the existing cloud data but disable iCloud ([manager migrateCloudStoreToLocal]). + * NOTE: The existing local store will be lost. + * NOTE: After doing this, it would be prudent to delete the cloud store ([manager deleteCloudStoreLocalOnly:NO]) + * so that enabling iCloud in the future will seed it with the new local store. + * - Delete the cloud store and recreate it by seeding it with the local store ([manager deleteCloudStoreLocalOnly:NO]). + * NOTE: The existing cloud store will be lost. + * - Rebuild the cloud content by seeding it with the cloud store of this device ([manager rebuildCloudContentFromCloudStore]). + * NOTE: Any cloud changes on other devices that failed to sync to this device will be lost. + * + * The recommended way to handle this method is to pop an alert to the user, tell him what happened, and give him + * a choice on how to proceed. The alert will pop up on each of his devices. If he wants to rebuild the cloud store, + * this will enable him to choose what device to press the rebuild button on. + * Don't forget to dismiss the alert when you get -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:YES. + * + * @param isCloudStore YES if the cloud store is currently loaded. + * NO if the local store is currently loaded. +*/ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionIsCloud:(BOOL)isCloudStore; + /** Triggered when the store manager fails to loads a persistence store. * * Useful to decide what to do to make a store available to the application. * * If you don't implement this method, the manager will disable the cloud store and fall back to the local store when loading the cloud store fails. It's the equivalent to implementing this method with `manager.cloudEnabled = NO;`. + * + * @param wasCloudStore YES if the error was caused while attempting to load the cloud store. + * NO if the error was caused while attempting to load the local store. */ @optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause context:(id)context wasCloud:(BOOL)wasCloudStore; +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause + context:(id)context wasCloud:(BOOL)wasCloudStore; /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions/logging in whatever way you see fit. * @@ -102,8 +181,9 @@ typedef enum { * * Implementing this method is required if you set -migrationStrategy to UbiquityStoreMigrationStrategyManual. * - * @param error Write out an error object here when the migration fails. - * @return YES when the migration was successful and the new store may be loaded. NO to error out and not load the new store (new store will be cleaned up if it exists). + * @param error If the migration fails, write out an error object that describes the problem. + * @return YES when the migration was successful and the new store may be loaded. + * NO to error out and not load the new store (new store will be cleaned up if it exists). */ @optional - (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager @@ -155,6 +235,17 @@ typedef enum { */ - (void)deleteLocalStore; +/** + * This will delete the local store and migrate the cloud store to a new local store. There is no recovery. + */ +- (void)migrateCloudStoreToLocal; + +/** + * This will delete the cloud content and recreate a new cloud store by seeding it with the current cloud store. + * Any cloud content and cloud store changes on other devices that are not present on this device's cloud store will be lost. There is no recovery. + */ +- (void)rebuildCloudContentFromCloudStore; + /** * Determine whether it's safe to seed the cloud store with a local store. */ diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index c8516d2..443ac98 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -7,6 +7,8 @@ // #import "UbiquityStoreManager.h" +#import "JRSwizzle.h" +#import "NSManagedObject+UbiquityStoreManager.h" #if TARGET_OS_IPHONE #import @@ -18,8 +20,8 @@ NSString *const UbiquityManagedStoreDidChangeNotification = @"UbiquityManagedStoreDidChangeNotification"; NSString *const UbiquityManagedStoreDidImportChangesNotification = @"UbiquityManagedStoreDidImportChangesNotification"; -NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. +NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. NSString *const CloudStoreDirectory = @"CloudStore.nosync"; NSString *const CloudLogsDirectory = @"CloudLogs"; @@ -44,6 +46,22 @@ @implementation UbiquityStoreManager { NSOperationQueue *_presentedItemOperationQueue; } ++ (void)initialize { + + if (![self respondsToSelector:@selector(jr_swizzleMethod:withMethod:error:)]) { + NSLog(@"UbiquityStoreManager: Warning: JRSwizzle not present, won't be able to detect desync issues."); + return; + } + + NSError *error = nil; + if (![NSError jr_swizzleMethod:@selector(initWithDomain:code:userInfo:) + withMethod:@selector(init_USM_WithDomain:code:userInfo:) + error:&error]) + NSLog(@"UbiquityStoreManager: Warning: Failed to swizzle, won't be able to detect desync issues. Cause: %@", error); + else + NSLog(@"UbiquityStoreManager: Swizzled (from %@).", self.class); +} + - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions delegate:(id )delegate { @@ -64,12 +82,14 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb _additionalStoreOptions = additionalStoreOptions == nil? [NSDictionary dictionary]: additionalStoreOptions; // Private vars. + _currentIdentityToken = [[NSFileManager defaultManager] ubiquityIdentityToken]; _migrationStrategy = UbiquityStoreMigrationStrategyCopyEntities; _persistentStorageQueue = [NSOperationQueue new]; _persistentStorageQueue.name = [NSString stringWithFormat:@"%@PersistenceQueue", NSStringFromClass([self class])]; _persistentStorageQueue.maxConcurrentOperationCount = 1; _presentedItemOperationQueue = [NSOperationQueue new]; _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass([self class])]; + _presentedItemOperationQueue.maxConcurrentOperationCount = 1; // Observe application events. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) @@ -78,6 +98,9 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cloudStoreChanged:) name:NSUbiquityIdentityDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(checkCloudContentCorruption:) + name:UbiquityManagedStoreDidDetectCorruptionNotification + object:nil]; #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification @@ -238,7 +261,7 @@ - (void)resetPersistentStoreCoordinator { - (void)clearStore { - [self log:@"Clearing stores, will load %@ store...", self.cloudEnabled?@"cloud": @"local"]; + [self log:@"Clearing stores..."]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; @@ -260,7 +283,7 @@ - (void)clearStore { - (void)loadStore { - [self log:@"Will load %@ store...", self.cloudEnabled ? @"cloud" : @"local"]; + [self log:@"(Re)loading store..."]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; @@ -279,9 +302,17 @@ - (void)loadStore { - (void)loadCloudStore { - NSError *error = nil; - UbiquityStoreErrorCause cause; + [self log:@"Will load cloud store: %@ (%@).", self.storeUUID, _tentativeStoreUUID? @"tentative": @"definite"]; + + // Check if the cloud store has been locked down because of content corruption. + if ([self checkCloudContentCorruption:nil]) + // We don't put this in the @try block because we don't want to handle this failure in the @finally block. + // That's because the check method allows the application to take action which would confuse the @finally block. + return; + id context = nil; + __block NSError *error = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; @@ -335,20 +366,30 @@ - (void)loadCloudStore { // Open local and cloud store. NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - NSPersistentStore *localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:localStoreURL - options:localStoreOptions - error:&error]; + __block NSPersistentStore *localStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:localStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:localStoreOptions + error:&error]; + }]; if (!localStore) { [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; break; } NSPersistentStoreCoordinator *cloudCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - NSPersistentStore *cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]; + __block NSPersistentStore *cloudStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:cloudStoreOptions + error:&error]; + }]; if (!cloudStore) { [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; @@ -399,11 +440,16 @@ - (void)loadCloudStore { } // Add the store now that migration is finished. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:cloudStoreOptions + error:&error]; + }]; + if (![self.persistentStoreCoordinator.persistentStores count]) + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } @@ -412,24 +458,32 @@ - (void)loadCloudStore { [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; // Add the store to migrate. - NSPersistentStore *localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:localStoreURL - options:localStoreOptions - error:&error]; - + __block NSPersistentStore *localStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:localStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:localStoreOptions + error:&error]; + }]; if (!localStore) { [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; break; } - if (![self.persistentStoreCoordinator migratePersistentStore:localStore - toURL:cloudStoreURL - options:cloudStoreOptions - withType:NSSQLiteStoreType - error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; - [self clearStore]; - } + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + if (![self.persistentStoreCoordinator migratePersistentStore:localStore + toURL:newURL + options:cloudStoreOptions + withType:NSSQLiteStoreType + error:&error]) + [self clearStore]; + }]; + if (![self.persistentStoreCoordinator.persistentStores count]) + [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } @@ -445,10 +499,15 @@ - (void)loadCloudStore { } // Add the store now that migration is finished. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:cloudStoreOptions + error:&error]; + }]; + if (![self.persistentStoreCoordinator.persistentStores count]) [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; @@ -458,10 +517,15 @@ - (void)loadCloudStore { [self log:@"Loading cloud store without local store migration."]; // Just add the store without first migrating to it. - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:cloudStoreURL - options:cloudStoreOptions - error:&error]) + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:&error byAccessor:^(NSURL *newURL) { + [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:cloudStoreOptions + error:&error]; + }]; + if (![self.persistentStoreCoordinator.persistentStores count]) [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } @@ -475,36 +539,38 @@ - (void)loadCloudStore { [userInfo setObject:error forKey:NSUnderlyingErrorKey]; [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = exception]; - [self clearStore]; } @finally { - BOOL cloudWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; - if (cloudWasEnabled) { + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. [self confirmTentativeStoreUUID]; - [self log:@"iCloud enabled (UUID:%@) and successfully loaded cloud store.", self.storeUUID]; + [self log:@"Cloud enabled and successfully loaded cloud store."]; } else { - // If this happens, the cloud store is desynced. - // Until it is either fixed or destroyed, the cloud store will be unavailable to the user. + // An error occurred in the @try block. [self resetTentativeStoreUUID]; + [self clearStore]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; + [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; } else { - [self log:@"iCloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; + [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; } } dispatch_async(dispatch_get_main_queue(), ^{ - if (cloudWasEnabled) { + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + // Store failed to load, inform delegate. [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; else + // Store failed to load, delegate doesn't care. Default strategy for cloud load failure: switch to local. self.cloudEnabled = NO; }); } @@ -512,8 +578,10 @@ - (void)loadCloudStore { - (void)loadLocalStore { - UbiquityStoreErrorCause cause; + [self log:@"Will load local store."]; + id context = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; @@ -533,6 +601,7 @@ - (void)loadLocalStore { } // Add local store to PSC. + [self log:@"Loading local store."]; NSURL *localStoreURL = [self URLForLocalStore]; if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:localStoreURL @@ -543,26 +612,31 @@ - (void)loadLocalStore { } } @finally { - BOOL localWasEnabled = [self.persistentStoreCoordinator.persistentStores count] > 0; - if (localWasEnabled) { - [self log:@"iCloud disabled and successfully loaded local store."]; + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. + [self log:@"Cloud disabled and successfully loaded local store."]; } else { - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:wasCloud:)]) { - [self log:@"iCloud disabled but failed to load local store (cause:%u, %@). Application will handle failure.", cause, context]; + // An error occurred in the @try block. + [self clearStore]; + + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { + [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Application will handle failure.", cause, context]; } else { - [self log:@"iCloud disabled but failed to load local store (cause:%u, %@). No store available to application.", cause, context]; + [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). No store available to application.", cause, context]; } } dispatch_async(dispatch_get_main_queue(), ^{ - if (localWasEnabled) { + if (cause == UbiquityStoreErrorCauseNoError) { + // Store loaded successfully. if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:NO]; [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + // Store failed to load, inform delegate. [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:NO]; }); } @@ -681,6 +755,7 @@ - (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self resetTentativeStoreUUID]; if (!localOnly) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreCorruptedKey]; [cloud removeObjectForKey:StoreUUIDKey]; [cloud synchronize]; } @@ -777,17 +852,8 @@ -(NSOperationQueue *)presentedItemOperationQueue { - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { - completionHandler(nil); - - // Reload the PSC's store. [self clearStore]; - - NSError *error; - if (self.cloudEnabled) - if (![[NSFileManager defaultManager] removeItemAtURL:[self URLForCloudStore] error:&error]) - [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:[self URLForCloudStore].path]; - - [self loadStore]; + completionHandler(nil); } @@ -821,13 +887,52 @@ - (void)keyValueStoreChanged:(NSNotification *)note { if ([changedKeys containsObject:StoreUUIDKey]) // The UUID of the active store changed. We need to switch to the newly activated store. [self cloudStoreChanged:nil]; + + if ([changedKeys containsObject:StoreCorruptedKey]) + // Cloud content corruption was detected or cleared. + [self checkCloudContentCorruption:nil]; + +} + +/** +* Triggered when: +* 1. Loading a cloud store. +* 2. StoreCorruptedKey changes are imported to the cloud KVS. +* 3. An NSError is created describing a transaction log import failure (UbiquityManagedStoreDidDetectCorruptionNotification). +*/ +- (BOOL)checkCloudContentCorruption:(NSNotification *)note { + + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + if (![cloud boolForKey:StoreCorruptedKey]) { + // No corruption detected. + + if (self.cloudEnabled && ![self.persistentStoreCoordinator.persistentStores count]) + // Corruption was removed. Try loading the store again. + [self loadStore]; + + return NO; + } + + // Cloud content corruption detected. + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:handleCloudContentCorruptionIsCloud:)]) { + [self.delegate ubiquityStoreManager:self handleCloudContentCorruptionIsCloud:self.cloudEnabled]; + + if (self.cloudEnabled) + // Since the cloud content is corrupt, we must unload the cloud store to prevent + // unsyncable changes from being made. + [self clearStore]; + } else + // Default strategy for corrupt cloud content: switch to local. + self.cloudEnabled = NO; + + return YES; } /** * Triggered when: - * 1. Ubiquity identity changed (eg. iCloud account changed in settings) - * 2. Store file was deleted (eg. iCloud container deleted in settings) - * 3. StoreUUID changed (eg. switched to a new cloud store on another device) + * 1. Ubiquity identity changed (NSUbiquityIdentityDidChangeNotification). + * 2. Store file was deleted (eg. iCloud container deleted in settings). + * 3. StoreUUID changed (eg. switched to a new cloud store on another device). */ - (void)cloudStoreChanged:(NSNotification *)note { @@ -847,7 +952,15 @@ - (void)mergeChanges:(NSNotification *)note { [self log:@"Importing ubiquity changes:\n%@", note.userInfo]; [self.persistentStorageQueue addOperationWithBlock:^{ - NSManagedObjectContext *moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; + NSManagedObjectContext *moc = nil; + if ([self.delegate respondsToSelector:@selector(managedObjectContextForUbiquityChangesInManager:)]) + moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; + if (!moc) { + moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; + moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; + moc.persistentStoreCoordinator = self.persistentStoreCoordinator; + } + [moc performBlockAndWait:^{ [moc mergeChangesFromContextDidSaveNotification:note]; @@ -859,13 +972,14 @@ - (void)mergeChanges:(NSNotification *)note { // If not, either the application will handle it or we'll fall back to the local store. // TODO: Verify that this works reliably. [self loadStore]; + return; } - }]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification object:self - userInfo:[note userInfo]]; - }); + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidImportChangesNotification + object:self userInfo:[note userInfo]]; + }); + }]; }]; } diff --git a/iCloudStoreManagerExample.xcodeproj/project.pbxproj b/iCloudStoreManagerExample.xcodeproj/project.pbxproj index 8afe2b2..132776c 100644 --- a/iCloudStoreManagerExample.xcodeproj/project.pbxproj +++ b/iCloudStoreManagerExample.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 5B4B126415227C6E00153613 /* iCloudStoreManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B126315227C6E00153613 /* iCloudStoreManagerTests.m */; }; 5B4B126F15227DFE00153613 /* User.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B126E15227DFE00153613 /* User.m */; }; 5B4B127215227DFE00153613 /* Event.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B127115227DFE00153613 /* Event.m */; }; + 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3982680A3617FB669BBC5 /* JRSwizzle.m */; }; + 93D39247AA48A93AE9BE359A /* NSManagedObject+UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */; }; DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA3B1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld */; }; DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */; }; /* End PBXBuildFile section */ @@ -71,6 +73,10 @@ 5B4B127015227DFE00153613 /* Event.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Event.h; sourceTree = ""; }; 5B4B127115227DFE00153613 /* Event.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Event.m; sourceTree = ""; }; 5B5E838C152BAB96009C1991 /* iCloudStoreManagerExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = iCloudStoreManagerExample.entitlements; sourceTree = ""; }; + 93D3982680A3617FB669BBC5 /* JRSwizzle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JRSwizzle.m; sourceTree = ""; }; + 93D39827ACFB3A3A3E1CEF4C /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JRSwizzle.h; sourceTree = ""; }; + 93D39B7366D5AD8A2FEFD7F1 /* NSManagedObject+UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+UbiquityStoreManager.h"; sourceTree = ""; }; + 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+UbiquityStoreManager.m"; sourceTree = ""; }; DA79AA3115585EBE00BAA07A /* iCloudStoreManagerExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iCloudStoreManagerExample-Info.plist"; sourceTree = ""; }; DA79AA3215585EBE00BAA07A /* iCloudStoreManagerExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iCloudStoreManagerExample-Prefix.pch"; sourceTree = ""; }; DA79AA3C1558608500BAA07A /* iCloudStoreManager.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iCloudStoreManager.xcdatamodel; sourceTree = ""; }; @@ -112,6 +118,7 @@ 5B4B125C15227C6E00153613 /* iCloudStoreManagerTests */, 5B4B122515227C6D00153613 /* Frameworks */, 5B4B122315227C6D00153613 /* Products */, + 93D3913AACE1E4369E3900DA /* jrswizzle */, ); sourceTree = ""; }; @@ -190,11 +197,22 @@ name = "Supporting Files"; sourceTree = ""; }; + 93D3913AACE1E4369E3900DA /* jrswizzle */ = { + isa = PBXGroup; + children = ( + 93D39827ACFB3A3A3E1CEF4C /* JRSwizzle.h */, + 93D3982680A3617FB669BBC5 /* JRSwizzle.m */, + ); + path = jrswizzle; + sourceTree = ""; + }; DA79AA3E1558613F00BAA07A /* iCloudStoreManager */ = { isa = PBXGroup; children = ( DA79AA3F1558613F00BAA07A /* UbiquityStoreManager.h */, DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */, + 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */, + 93D39B7366D5AD8A2FEFD7F1 /* NSManagedObject+UbiquityStoreManager.h */, ); path = iCloudStoreManager; sourceTree = ""; @@ -317,6 +335,8 @@ 5B4B127215227DFE00153613 /* Event.m in Sources */, DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */, DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */, + 93D39247AA48A93AE9BE359A /* NSManagedObject+UbiquityStoreManager.m in Sources */, + 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/jrswizzle b/jrswizzle new file mode 160000 index 0000000..98d18ae --- /dev/null +++ b/jrswizzle @@ -0,0 +1 @@ +Subproject commit 98d18aee73329321c320a2df85bacdb9f08a34a6 From aa099ede3ee62aab67d192510584507529540c67 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 15 Mar 2013 23:53:48 -0400 Subject: [PATCH 25/35] Implement cloud content corruption recovery methods. [ADDED] A method for migrating the cloud store to the local store. [ADDED] A method for rebuilding cloud content from the cloud store. --- .idea/inspectionProfiles/Project_Default.xml | 7 + .../inspectionProfiles/profiles_settings.xml | 7 + iCloudStoreManager/UbiquityStoreManager.h | 33 ++- iCloudStoreManager/UbiquityStoreManager.m | 243 ++++++++++++------ 4 files changed, 196 insertions(+), 94 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..9a56338 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index abe0bfa..cc99362 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -51,9 +51,11 @@ typedef enum { UbiquityStoreErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. context = the path of the store. UbiquityStoreErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. context = the path of the store. UbiquityStoreErrorCauseClearStore, // Error occurred while removing a store from the coordinator. context = the store. - UbiquityStoreErrorCauseOpenLocalStore, // Error occurred while opening the local store file. context = the path of the store. - UbiquityStoreErrorCauseOpenCloudStore, // Error occurred while opening the cloud store file. context = the path of the store. - UbiquityStoreErrorCauseMigrateLocalToCloudStore, // Error occurred while migrating the local store to the cloud. context = the path of the store or exception that caused the problem. + UbiquityStoreErrorCauseOpenLocalStore, // Error occurred while opening the local store. context = the path of the store. + UbiquityStoreErrorCauseOpenCloudStore, // Error occurred while opening the cloud store. context = the path of the store. + UbiquityStoreErrorCauseOpenMigrationStore, // Error occurred while opening the migration store. context = the path of the store. + UbiquityStoreErrorCauseMigrateToCloudStore, // Error occurred while seeding the cloud content. context = the path of the migrating store or exception that caused the problem. + UbiquityStoreErrorCauseMigrateToLocalStore, // Error occurred while seeding the local store. context = the path of the migrating store. UbiquityStoreErrorCauseImportChanges, // Error occurred while importing changes from the cloud into the application's context. context = the DidImportUbiquitousContentChanges notification. } UbiquityStoreErrorCause; @@ -129,12 +131,12 @@ typedef enum { * When you receive this method, there are a few things you can do to handle the situation: * - Switch to the local store (manager.cloudEnabled = NO). * NOTE: The cloud data and cloud syncing will be unavailable. - * - Keep the existing cloud data but disable iCloud ([manager migrateCloudStoreToLocal]). - * NOTE: The existing local store will be lost. - * NOTE: After doing this, it would be prudent to delete the cloud store ([manager deleteCloudStoreLocalOnly:NO]) - * so that enabling iCloud in the future will seed it with the new local store. * - Delete the cloud store and recreate it by seeding it with the local store ([manager deleteCloudStoreLocalOnly:NO]). * NOTE: The existing cloud store will be lost. + * - Keep the existing cloud data but disable iCloud ([manager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]). + * NOTE: The existing local store will be lost. + * NOTE: You should set localOnly to NO so that the corruption is cleared and enabling iCloud in the future + * will seed it with the new local store. * - Rebuild the cloud content by seeding it with the cloud store of this device ([manager rebuildCloudContentFromCloudStore]). * NOTE: Any cloud changes on other devices that failed to sync to this device will be lost. * @@ -217,7 +219,8 @@ typedef enum { /** * This will delete all the data from iCloud for this application. * - * @param localOnly If YES, the iCloud data will be redownloaded when needed. If NO, the container's data will be permanently lost. + * @param localOnly If YES, the iCloud data will be redownloaded when needed. + * If NO, the container's data will be permanently lost. * * Unless you intend to delete more than just the active cloud store, you should probably use -deleteCloudStoreLocalOnly: instead. */ @@ -226,23 +229,27 @@ typedef enum { /** * This will delete the iCloud store. * - * @param localOnly If YES, the iCloud transaction logs will be redownloaded and the store rebuilt. If NO, the store will be permanently lost and a new one will be created by migrating the device's local store. + * @param localOnly If YES, the iCloud transaction logs will be redownloaded and the store rebuilt. + * If NO, the store will be permanently lost and a new one will be created by migrating the device's local store. */ - (void)deleteCloudStoreLocalOnly:(BOOL)localOnly; /** - * This will delete the local store. There is no recovery. + * This will delete the local store. */ - (void)deleteLocalStore; /** - * This will delete the local store and migrate the cloud store to a new local store. There is no recovery. + * This will delete the local store and migrate the cloud store to a new local store. The cloud store is subsequently deleted. The device will subsequently load the new local store (disable cloud). + * + * @param localOnly If YES, the cloud content is not deleted from iCloud. + * If NO, the cloud store will be permanently lost and a new one will be created by migrating the new local store when iCloud is re-enabled. */ -- (void)migrateCloudStoreToLocal; +- (void)migrateCloudToLocalAndDeleteCloudStoreLocalOnly:(BOOL)localOnly; /** * This will delete the cloud content and recreate a new cloud store by seeding it with the current cloud store. - * Any cloud content and cloud store changes on other devices that are not present on this device's cloud store will be lost. There is no recovery. + * Any cloud content and cloud store changes on other devices that are not present on this device's cloud store will be lost. */ - (void)rebuildCloudContentFromCloudStore; diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 443ac98..d115aa4 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -23,7 +23,8 @@ NSString *const CloudEnabledKey = @"USMCloudEnabledKey"; // local: Whether the user wants the app on this device to use iCloud. NSString *const StoreUUIDKey = @"USMStoreUUIDKey"; // cloud: The UUID of the active cloud store. NSString *const CloudStoreDirectory = @"CloudStore.nosync"; -NSString *const CloudLogsDirectory = @"CloudLogs"; +NSString *const CloudStoreMigrationSource = @"MigrationSource.sqlite"; +NSString *const CloudContentDirectory = @"CloudLogs"; @interface UbiquityStoreManager () @@ -36,8 +37,8 @@ @interface UbiquityStoreManager () @property (nonatomic, strong) NSString *tentativeStoreUUID; @property (nonatomic, strong) NSOperationQueue *persistentStorageQueue; @property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; - -@property(nonatomic, strong) id currentIdentityToken; +@property (nonatomic, strong) id currentIdentityToken; +@property (nonatomic, strong) NSURL *migrationStoreURL; @end @@ -179,7 +180,7 @@ - (NSURL *)URLForCloudStore { - (NSURL *)URLForCloudContentDirectory { // The transaction logs are in the ubiquity container and are synced by iCloud. - return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudLogsDirectory isDirectory:YES]; + return [[self URLForCloudContainer] URLByAppendingPathComponent:CloudContentDirectory isDirectory:YES]; } - (NSURL *)URLForCloudContent { @@ -310,9 +311,9 @@ - (void)loadCloudStore { // That's because the check method allows the application to take action which would confuse the @finally block. return; - id context = nil; + __block id context = nil; __block NSError *error = nil; - UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + __block UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; @@ -324,7 +325,7 @@ - (void)loadCloudStore { // Create the path to the cloud store. NSURL *cloudStoreURL = [self URLForCloudStore]; - NSURL *localStoreURL = [self URLForLocalStore]; + NSURL *migrationStoreURL = self.migrationStoreURL? self.migrationStoreURL: [self localStoreURL]; NSURL *cloudStoreContentURL = [self URLForCloudContent]; NSURL *cloudStoreDirectoryURL = [self URLForCloudStoreDirectory]; BOOL storeExists = [[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]; @@ -349,15 +350,15 @@ - (void)loadCloudStore { @YES, NSMigratePersistentStoresAutomaticallyOption, @YES, NSInferMappingModelAutomaticallyOption, nil]; - NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + NSMutableDictionary *migrationStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: @YES, NSReadOnlyPersistentStoreOption, nil]; [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; // Now load the cloud store. If possible, first migrate the local store to it. UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; - if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) + if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:migrationStoreURL.path]) migrationStrategy = UbiquityStoreMigrationStrategyNone; switch (migrationStrategy) { @@ -366,17 +367,17 @@ - (void)loadCloudStore { // Open local and cloud store. NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - __block NSPersistentStore *localStore = nil; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:localStoreURL + __block NSPersistentStore *migrationStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 error:&error byAccessor:^(NSURL *newURL) { - localStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType + migrationStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newURL - options:localStoreOptions + options:migrationStoreOptions error:&error]; }]; - if (!localStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; + if (!migrationStore) { + [self error:error cause:cause = UbiquityStoreErrorCauseOpenMigrationStore context:context = migrationStoreURL.path]; break; } @@ -402,7 +403,7 @@ - (void)loadCloudStore { cloudContext.persistentStoreCoordinator = cloudCoordinator; // Copy metadata. - NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:localStore] mutableCopy]; + NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:migrationStore] mutableCopy]; [metadata addEntriesFromDictionary:[cloudCoordinator metadataForPersistentStore:cloudStore]]; [cloudCoordinator setMetadata:metadata forPersistentStore:cloudStore]; @@ -431,7 +432,7 @@ - (void)loadCloudStore { // Handle failure by cleaning up the cloud store. if (migrationFailure) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) [self error:error cause:cause = UbiquityStoreErrorCauseClearStore context:cloudStore]; @@ -458,32 +459,31 @@ - (void)loadCloudStore { [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; // Add the store to migrate. - __block NSPersistentStore *localStore = nil; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:localStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - localStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:localStoreOptions - error:&error]; - }]; - if (!localStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; - break; - } + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 + writingItemAtURL:cloudStoreURL options:NSFileCoordinatorWritingForMerging + error:&error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + NSPersistentStore *migrationStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newReadingURL + options:migrationStoreOptions + error:&error]; + if (!migrationStore) { + [self error:error cause:cause = UbiquityStoreErrorCauseOpenMigrationStore context:context = migrationStoreURL.path]; + return; + } + + if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore + toURL:newWritingURL + options:cloudStoreOptions + withType:NSSQLiteStoreType + error:&error]) { + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; + return; + } + }]; + if (cause != UbiquityStoreErrorCauseNoError) + return; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - if (![self.persistentStoreCoordinator migratePersistentStore:localStore - toURL:newURL - options:cloudStoreOptions - withType:NSSQLiteStoreType - error:&error]) - [self clearStore]; - }]; - if (![self.persistentStoreCoordinator.persistentStores count]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; break; } @@ -491,9 +491,9 @@ - (void)loadCloudStore { [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyManual"]; if (![self.delegate ubiquityStoreManager:self - manuallyMigrateStore:localStoreURL withOptions:localStoreOptions + manuallyMigrateStore:migrationStoreURL withOptions:migrationStoreOptions toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = cloudStoreURL.path]; + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; [self removeItemAtURL:cloudStoreURL localOnly:NO]; break; } @@ -538,9 +538,11 @@ - (void)loadCloudStore { if (error) [userInfo setObject:error forKey:NSUnderlyingErrorKey]; [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] - cause:cause = UbiquityStoreErrorCauseMigrateLocalToCloudStore context:context = exception]; + cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = exception]; } @finally { + self.migrationStoreURL = nil; + if (cause == UbiquityStoreErrorCauseNoError) { // Store loaded successfully. [self confirmTentativeStoreUUID]; @@ -552,9 +554,9 @@ - (void)loadCloudStore { [self clearStore]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { - [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Application will handle failure.", cause, context]; + [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Application will handle.", cause, context]; } else { - [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Will fall back to local store.", cause, context]; + [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Handling with default strategy: falling back to local store.", cause, context]; } } @@ -580,13 +582,13 @@ - (void)loadLocalStore { [self log:@"Will load local store."]; - id context = nil; - UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + __block id context = nil; + __block UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; // Load local store if iCloud is disabled. - NSError *error = nil; + __block NSError *error = nil; NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: @YES, NSMigratePersistentStoresAutomaticallyOption, @YES, NSInferMappingModelAutomaticallyOption, @@ -600,9 +602,23 @@ - (void)loadLocalStore { return; } + // If the local store doesn't exist yet and a migrationStore is set, copy it. + NSURL *localStoreURL = [self URLForLocalStore]; + if ([[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path] && + ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) { + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:self.migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 + writingItemAtURL:self.localStoreURL options:NSFileCoordinatorWritingForReplacing + error:&error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + if (![[NSFileManager defaultManager] copyItemAtURL:newReadingURL toURL:newWritingURL error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToLocalStore context:context = self.migrationStoreURL.path]; + }]; + if (cause != UbiquityStoreErrorCauseNoError) + return; + } + // Add local store to PSC. [self log:@"Loading local store."]; - NSURL *localStoreURL = [self URLForLocalStore]; if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:localStoreURL options:localStoreOptions @@ -612,6 +628,8 @@ - (void)loadLocalStore { } } @finally { + self.migrationStoreURL = nil; + if (cause == UbiquityStoreErrorCauseNoError) { // Store loaded successfully. [self log:@"Cloud disabled and successfully loaded local store."]; @@ -621,9 +639,9 @@ - (void)loadLocalStore { [self clearStore]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { - [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Application will handle failure.", cause, context]; + [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Application will handle.", cause, context]; } else { - [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). No store available to application.", cause, context]; + [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Handling with default strategy: no store available.", cause, context]; } } @@ -721,21 +739,23 @@ - (void)deleteCloudContainerLocalOnly:(BOOL)localOnly { [self.persistentStorageQueue addOperationWithBlock:^{ [self log:@"Will delete the cloud container %@.", localOnly ? @"on this device" : @"on this device and in the cloud"]; + if (self.cloudEnabled) [self clearStore]; - // Delete the whole cloud container. - [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; + // Delete the whole cloud container. + [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; - // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; - if (!localOnly) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud synchronize]; - for (id key in [[cloud dictionaryRepresentation] allKeys]) - [cloud removeObjectForKey:key]; - [cloud synchronize]; - } + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + for (id key in [[cloud dictionaryRepresentation] allKeys]) + [cloud removeObjectForKey:key]; + [cloud synchronize]; + } + if (self.cloudEnabled) [self loadStore]; }]; } @@ -745,21 +765,23 @@ - (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self.persistentStorageQueue addOperationWithBlock:^{ [self log:@"Will delete the cloud store (UUID:%@) %@.", self.storeUUID, localOnly ? @"on this device" : @"on this device and in the cloud"]; + if (self.cloudEnabled) [self clearStore]; - // Clean up any cloud stores and transaction logs. - [self removeItemAtURL:[self URLForCloudStoreDirectory] localOnly:localOnly]; - [self removeItemAtURL:[self URLForCloudContentDirectory] localOnly:localOnly]; + // Clean up any cloud stores and transaction logs. + [self removeItemAtURL:[self URLForCloudStore] localOnly:localOnly]; + [self removeItemAtURL:[self URLForCloudContent] localOnly:localOnly]; - // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; - if (!localOnly) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreCorruptedKey]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - } + // Unset the storeUUID so a new one will be created. + [self resetTentativeStoreUUID]; + if (!localOnly) { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud removeObjectForKey:StoreCorruptedKey]; + [cloud removeObjectForKey:StoreUUIDKey]; + [cloud synchronize]; + } + if (self.cloudEnabled) [self loadStore]; }]; } @@ -769,15 +791,68 @@ - (void)deleteLocalStore { [self.persistentStorageQueue addOperationWithBlock:^{ [self log:@"Will delete the local store."]; + if (!self.cloudEnabled) [self clearStore]; - // Remove just the local store. - [self removeItemAtURL:[self URLForLocalStoreDirectory] localOnly:YES]; + // Remove just the local store. + [self removeItemAtURL:[self URLForLocalStore] localOnly:YES]; + if (!self.cloudEnabled) [self loadStore]; }]; } +- (void)migrateCloudToLocalAndDeleteCloudStoreLocalOnly:(BOOL)localOnly { + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will overwrite the local store with the cloud store."]; + + [self clearStore]; + + self.migrationStoreURL = [self URLForCloudStore]; + if (![[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path]) { + [self log:@"Cannot migrate cloud to local: Cloud store doesn't exist."]; + self.migrationStoreURL = nil; + } + + [self deleteLocalStore]; + self.cloudEnabled = NO; + [self deleteCloudStoreLocalOnly:localOnly]; + }]; +} + +- (void)rebuildCloudContentFromCloudStore { + + [self.persistentStorageQueue addOperationWithBlock:^{ + [self log:@"Will rebuild cloud content from the cloud store."]; + + [self clearStore]; + + NSURL *cloudStoreURL = [self URLForCloudStore]; + if (![[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]) { + [self log:@"Cannot rebuild cloud content: Cloud store doesn't exist."]; + } + else { + __block BOOL success = NO; + __block NSError *error = nil; + self.migrationStoreURL = [[self URLForCloudStoreDirectory] URLByAppendingPathComponent:CloudStoreMigrationSource isDirectory:NO]; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL options:(NSFileCoordinatorReadingOptions) 0 + writingItemAtURL:self.migrationStoreURL options:NSFileCoordinatorWritingForReplacing + error:&error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + success = [[NSFileManager defaultManager] moveItemAtURL:newReadingURL toURL:newWritingURL error :&error]; + }]; + if (!success) { + [self error:error cause:UbiquityStoreErrorCauseMigrateToCloudStore context:self.migrationStoreURL.path]; + return; + } + } + + [self deleteCloudStoreLocalOnly:NO]; + self.cloudEnabled = YES; + }]; +} + #pragma mark - Properties - (BOOL)cloudEnabled { @@ -905,6 +980,7 @@ - (BOOL)checkCloudContentCorruption:(NSNotification *)note { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; if (![cloud boolForKey:StoreCorruptedKey]) { // No corruption detected. + [self log:@"Cloud content corruption cleared."]; if (self.cloudEnabled && ![self.persistentStoreCoordinator.persistentStores count]) // Corruption was removed. Try loading the store again. @@ -915,15 +991,18 @@ - (BOOL)checkCloudContentCorruption:(NSNotification *)note { // Cloud content corruption detected. if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:handleCloudContentCorruptionIsCloud:)]) { + [self log:@"Cloud content corruption detected. Application will handle."]; [self.delegate ubiquityStoreManager:self handleCloudContentCorruptionIsCloud:self.cloudEnabled]; if (self.cloudEnabled) // Since the cloud content is corrupt, we must unload the cloud store to prevent // unsyncable changes from being made. [self clearStore]; - } else + } else { // Default strategy for corrupt cloud content: switch to local. + [self log:@"Cloud content corruption detected. Handling with default strategy: Falling back to local store."]; self.cloudEnabled = NO; + } return YES; } @@ -950,12 +1029,14 @@ - (void)cloudStoreChanged:(NSNotification *)note { - (void)mergeChanges:(NSNotification *)note { - [self log:@"Importing ubiquity changes:\n%@", note.userInfo]; [self.persistentStorageQueue addOperationWithBlock:^{ NSManagedObjectContext *moc = nil; if ([self.delegate respondsToSelector:@selector(managedObjectContextForUbiquityChangesInManager:)]) moc = [self.delegate managedObjectContextForUbiquityChangesInManager:self]; - if (!moc) { + if (moc) + [self log:@"Importing ubiquity changes into application's MOC. Changes:\n%@", note.userInfo]; + else { + [self log:@"Importing ubiquity changes with default strategy: into persistence store. Changes:\n%@", note.userInfo]; moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; moc.persistentStoreCoordinator = self.persistentStoreCoordinator; From b3b7626734625a4764b61c5de8c2cd0955eb0457 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 16 Mar 2013 00:47:02 -0400 Subject: [PATCH 26/35] Fixes to cloud corruption handling and implement in example. [FIXED] Build fixes for cloud corruption handling code. [FIXED] Only reload the store on lack of corruption when it's detected as a cloud KVS change to avoid infinite-reloading of a non-corrupt cloud store. [ADDED] Recommended application handling code to the example. --- .../NSManagedObject+UbiquityStoreManager.h | 4 +- .../NSManagedObject+UbiquityStoreManager.m | 4 +- iCloudStoreManager/UbiquityStoreManager.m | 13 ++--- iCloudStoreManagerExample/AppDelegate.h | 2 +- iCloudStoreManagerExample/AppDelegate.m | 48 +++++++++++++++++++ 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h index 7e31f6d..3465023 100644 --- a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h +++ b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h @@ -7,8 +7,8 @@ #import -NSString *const UbiquityManagedStoreDidDetectCorruptionNotification = @"UbiquityManagedStoreDidDetectCorruptionNotification"; -NSString *const StoreCorruptedKey = @"USMStoreCorruptedKey"; // cloud: Set to YES when a cloud content corruption has been detected. +extern NSString *const UbiquityManagedStoreDidDetectCorruptionNotification; +extern NSString *const StoreCorruptedKey; @interface NSError (UbiquityStoreManager) diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m index ff34aab..445552f 100644 --- a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m +++ b/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m @@ -7,6 +7,8 @@ #import "NSManagedObject+UbiquityStoreManager.h" +NSString *const UbiquityManagedStoreDidDetectCorruptionNotification = @"UbiquityManagedStoreDidDetectCorruptionNotification"; +NSString *const StoreCorruptedKey = @"USMStoreCorruptedKey"; // cloud: Set to YES when a cloud content corruption has been detected. @implementation NSError (UbiquityStoreManager) @@ -16,7 +18,7 @@ - (id)init_USM_WithDomain:(NSString *)domain code:(NSInteger)code userInfo:(NSDi if ([domain isEqualToString:NSCocoaErrorDomain] && code == 134302) { NSLog(@"Detected iCloud transaction log import failure: %@", self); NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud setValue:@YES forKeyPath:StoreCorruptedKey]; + [cloud setBool:YES forKey:StoreCorruptedKey]; [cloud synchronize]; [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidDetectCorruptionNotification diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index d115aa4..826deec 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -965,8 +965,10 @@ - (void)keyValueStoreChanged:(NSNotification *)note { if ([changedKeys containsObject:StoreCorruptedKey]) // Cloud content corruption was detected or cleared. - [self checkCloudContentCorruption:nil]; - + if (![self checkCloudContentCorruption:nil]) + if (self.cloudEnabled && ![self.persistentStoreCoordinator.persistentStores count]) + // Corruption was removed and our cloud store is not yet loaded. Try loading the store again. + [self loadStore]; } /** @@ -979,12 +981,7 @@ - (BOOL)checkCloudContentCorruption:(NSNotification *)note { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; if (![cloud boolForKey:StoreCorruptedKey]) { - // No corruption detected. - [self log:@"Cloud content corruption cleared."]; - - if (self.cloudEnabled && ![self.persistentStoreCoordinator.persistentStores count]) - // Corruption was removed. Try loading the store again. - [self loadStore]; + // [self loadStore]; return NO; } diff --git a/iCloudStoreManagerExample/AppDelegate.h b/iCloudStoreManagerExample/AppDelegate.h index e95fb85..088573f 100644 --- a/iCloudStoreManagerExample/AppDelegate.h +++ b/iCloudStoreManagerExample/AppDelegate.h @@ -11,7 +11,7 @@ @class User; -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index 6fd6e4b..c3acadf 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -13,6 +13,8 @@ #import "User.h" @interface AppDelegate () +@property(nonatomic, strong) UIAlertView *handleCloudContentAlert; + - (NSURL *)storeURL; @end @@ -148,6 +150,27 @@ - (User *)primaryUser { return primaryUser; } +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { + + if (alertView == self.handleCloudContentAlert) { + if (buttonIndex == [alertView firstOtherButtonIndex]) + // Disable iCloud + self.ubiquityStoreManager.cloudEnabled = NO; + else if (buttonIndex == [alertView firstOtherButtonIndex] + 1) + // Lose iCloud data + [self.ubiquityStoreManager deleteCloudStoreLocalOnly:NO]; + else if (buttonIndex == [alertView firstOtherButtonIndex] + 2) + // Make iCloud local + [self.ubiquityStoreManager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]; + else if (buttonIndex == [alertView firstOtherButtonIndex] + 3) + // Fix iCloud + [self.ubiquityStoreManager rebuildCloudContentFromCloudStore]; + } +} + + #pragma mark - UbiquityStoreManagerDelegate // STEP 4 - Implement the UbiquityStoreManager delegate methods @@ -180,9 +203,34 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoor __managedObjectContext = moc; __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; dispatch_async(dispatch_get_main_queue(), ^{ + if ([self.handleCloudContentAlert isVisible]) + [self.handleCloudContentAlert dismissWithClickedButtonIndex:[self.handleCloudContentAlert cancelButtonIndex] + animated:YES]; + [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; [masterViewController.storeLoadingActivity stopAnimating]; }); } +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionIsCloud:(BOOL)isCloudStore { + + dispatch_async(dispatch_get_main_queue(), ^{ + self.handleCloudContentAlert = [[UIAlertView alloc] initWithTitle:@"Problem With iCloud Sync!" message: + @"An error has occurred within Apple's iCloud causing your devices to no longer " + @"sync up properly.\n" + @"To fix this, you can either:\n" + @"- Disable iCloud\n" + @"- Lose your iCloud data and start anew using your local data\n" + @"- Make iCloud local and disable iCloud sync\n" + @"- Fix iCloud sync\n\n" + @"If you 'Make iCloud local', iCloud data will overwrite any local data you may have.\n\n" + @"If you 'Fix iCloud' (same as 'Make iCloud local' and turning iCloud sync on again later), " + @"be mindful on what device you do this on: Any changes on other devices that failed to sync " + @"will be lost." + delegate:self cancelButtonTitle:nil + otherButtonTitles:@"Disable iCloud", @"Lose iCloud data", @"Make iCloud local", @"Fix iCloud", nil]; + [self.handleCloudContentAlert show]; + }); +} + @end From 5326db7634eead93241fe27369ab0cf71b069b1a Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sat, 16 Mar 2013 17:21:36 -0400 Subject: [PATCH 27/35] Migration code improvement, esp. cloud -> local. [UPDATED] Unified migration code and refactoring of store loading. [FIXED] Cloud -> local should happen with migration too, not just copying. [FIXED] When migrating by copying data, don't copy ubiquity metadata: Local stores may not have it and target cloud stores have their own. [FIXED] Add cloud options when opening a cloud migration store. [FIXED] Synchronize KVS before accessing its keys to get more up-to-date values. [IMPROVED] Slightly improved logging output. --- iCloudStoreManager/UbiquityStoreManager.h | 8 +- iCloudStoreManager/UbiquityStoreManager.m | 482 +++++++++++----------- 2 files changed, 255 insertions(+), 235 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index cc99362..94101c2 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -51,11 +51,9 @@ typedef enum { UbiquityStoreErrorCauseDeleteStore, // Error occurred while deleting the store file or its transaction logs. context = the path of the store. UbiquityStoreErrorCauseCreateStorePath, // Error occurred while creating the path where the store needs to be saved. context = the path of the store. UbiquityStoreErrorCauseClearStore, // Error occurred while removing a store from the coordinator. context = the store. - UbiquityStoreErrorCauseOpenLocalStore, // Error occurred while opening the local store. context = the path of the store. - UbiquityStoreErrorCauseOpenCloudStore, // Error occurred while opening the cloud store. context = the path of the store. - UbiquityStoreErrorCauseOpenMigrationStore, // Error occurred while opening the migration store. context = the path of the store. - UbiquityStoreErrorCauseMigrateToCloudStore, // Error occurred while seeding the cloud content. context = the path of the migrating store or exception that caused the problem. - UbiquityStoreErrorCauseMigrateToLocalStore, // Error occurred while seeding the local store. context = the path of the migrating store. + UbiquityStoreErrorCauseOpenActiveStore, // Error occurred while opening the active store. context = the path of the store. + UbiquityStoreErrorCauseOpenSeedStore, // Error occurred while opening the seed store. context = the path of the store. + UbiquityStoreErrorCauseSeedStore, // Error occurred while seeding the store. context = the path of the seed store. UbiquityStoreErrorCauseImportChanges, // Error occurred while importing changes from the cloud into the application's context. context = the DidImportUbiquitousContentChanges notification. } UbiquityStoreErrorCause; diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 826deec..fafa1b4 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -323,27 +323,36 @@ - (void)loadCloudStore { return; } - // Create the path to the cloud store. + // Create the path to the cloud store and content if it doesn't exist yet. NSURL *cloudStoreURL = [self URLForCloudStore]; - NSURL *migrationStoreURL = self.migrationStoreURL? self.migrationStoreURL: [self localStoreURL]; NSURL *cloudStoreContentURL = [self URLForCloudContent]; NSURL *cloudStoreDirectoryURL = [self URLForCloudStoreDirectory]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreDirectoryURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreDirectoryURL.path]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreContentURL.path + withIntermediateDirectories:YES attributes:nil error:&error]) + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreContentURL.path]; + + // Clean up the cloud store if the cloud content got deleted. BOOL storeExists = [[NSFileManager defaultManager] fileExistsAtPath:cloudStoreURL.path]; BOOL storeContentExists = [[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:cloudStoreContentURL error:nil]; if (storeExists && !storeContentExists) { // We have a cloud store but no cloud content. The cloud content was deleted: // The existing store cannot sync anymore and needs to be recreated. + [self log:@"Deleting cloud store: it has no cloud content."]; + if (![[NSFileManager defaultManager] removeItemAtURL:cloudStoreURL error:&error]) [self error:error cause:cause = UbiquityStoreErrorCauseDeleteStore context:context = cloudStoreURL.path]; } - if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreDirectoryURL.path - withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreDirectoryURL.path]; - if (![[NSFileManager defaultManager] createDirectoryAtPath:cloudStoreContentURL.path - withIntermediateDirectories:YES attributes:nil error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = cloudStoreContentURL.path]; - // Add cloud store to PSC. + // Check if we need to seed the store by migrating another store into it. + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; + NSURL *migrationStoreURL = self.migrationStoreURL ? self.migrationStoreURL : [self localStoreURL]; + if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:migrationStoreURL.path]) + migrationStrategy = UbiquityStoreMigrationStrategyNone; + + // Load the cloud store. NSMutableDictionary *cloudStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: self.contentName, NSPersistentStoreUbiquitousContentNameKey, cloudStoreContentURL, NSPersistentStoreUbiquitousContentURLKey, @@ -355,190 +364,9 @@ - (void)loadCloudStore { nil]; [cloudStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; - - // Now load the cloud store. If possible, first migrate the local store to it. - UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; - if (![self cloudSafeForSeeding] || ![[NSFileManager defaultManager] fileExistsAtPath:migrationStoreURL.path]) - migrationStrategy = UbiquityStoreMigrationStrategyNone; - - switch (migrationStrategy) { - case UbiquityStoreMigrationStrategyCopyEntities: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; - - // Open local and cloud store. - NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - __block NSPersistentStore *migrationStore = nil; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - migrationStore = [localCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:migrationStoreOptions - error:&error]; - }]; - if (!migrationStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenMigrationStore context:context = migrationStoreURL.path]; - break; - } - - NSPersistentStoreCoordinator *cloudCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; - __block NSPersistentStore *cloudStore = nil; - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - cloudStore = [cloudCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:cloudStoreOptions - error:&error]; - }]; - if (!cloudStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - break; - } - - // Set up contexts for them. - NSManagedObjectContext *localContext = [NSManagedObjectContext new]; - NSManagedObjectContext *cloudContext = [NSManagedObjectContext new]; - localContext.persistentStoreCoordinator = localCoordinator; - cloudContext.persistentStoreCoordinator = cloudCoordinator; - - // Copy metadata. - NSMutableDictionary *metadata = [[localCoordinator metadataForPersistentStore:migrationStore] mutableCopy]; - [metadata addEntriesFromDictionary:[cloudCoordinator metadataForPersistentStore:cloudStore]]; - [cloudCoordinator setMetadata:metadata forPersistentStore:cloudStore]; - - // Migrate entities. - BOOL migrationFailure = NO; - NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; - for (NSEntityDescription *entity in self.model.entities) { - NSFetchRequest *fetch = [NSFetchRequest new]; - fetch.entity = entity; - fetch.fetchBatchSize = 500; - fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; - - NSArray *localObjects = [localContext executeFetchRequest:fetch error:&error]; - if (!localObjects) { - migrationFailure = YES; - break; - } - - for (NSManagedObject *localObject in localObjects) - [self copyMigrateObject:localObject toContext:cloudContext usingMigrationCache:migratedIDsBySourceID]; - } - - // Save migrated entities. - if (!migrationFailure && ![cloudContext save:&error]) - migrationFailure = YES; - - // Handle failure by cleaning up the cloud store. - if (migrationFailure) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; - - if (![cloudCoordinator removePersistentStore:cloudStore error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseClearStore context:cloudStore]; - [self removeItemAtURL:cloudStoreURL localOnly:NO]; - break; - } - - // Add the store now that migration is finished. - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:cloudStoreOptions - error:&error]; - }]; - if (![self.persistentStoreCoordinator.persistentStores count]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - - break; - } - - case UbiquityStoreMigrationStrategyIOS: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyIOS"]; - - // Add the store to migrate. - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 - writingItemAtURL:cloudStoreURL options:NSFileCoordinatorWritingForMerging - error:&error byAccessor: - ^(NSURL *newReadingURL, NSURL *newWritingURL) { - NSPersistentStore *migrationStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newReadingURL - options:migrationStoreOptions - error:&error]; - if (!migrationStore) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenMigrationStore context:context = migrationStoreURL.path]; - return; - } - - if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore - toURL:newWritingURL - options:cloudStoreOptions - withType:NSSQLiteStoreType - error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; - return; - } - }]; - if (cause != UbiquityStoreErrorCauseNoError) - return; - - break; - } - - case UbiquityStoreMigrationStrategyManual: { - [self log:@"Migrating local store to new cloud store using strategy: UbiquityStoreMigrationStrategyManual"]; - - if (![self.delegate ubiquityStoreManager:self - manuallyMigrateStore:migrationStoreURL withOptions:migrationStoreOptions - toStore:cloudStoreURL withOptions:cloudStoreOptions error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = migrationStoreURL.path]; - [self removeItemAtURL:cloudStoreURL localOnly:NO]; - break; - } - - // Add the store now that migration is finished. - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:cloudStoreOptions - error:&error]; - }]; - if (![self.persistentStoreCoordinator.persistentStores count]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - - break; - } - - case UbiquityStoreMigrationStrategyNone: { - [self log:@"Loading cloud store without local store migration."]; - - // Just add the store without first migrating to it. - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL - options:(NSFileCoordinatorReadingOptions) 0 - error:&error byAccessor:^(NSURL *newURL) { - [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:newURL - options:cloudStoreOptions - error:&error]; - }]; - if (![self.persistentStoreCoordinator.persistentStores count]) - [self error:error cause:cause = UbiquityStoreErrorCauseOpenCloudStore context:context = cloudStoreURL.path]; - break; - } - } - } - @catch (id exception) { - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; - if (exception) - [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; - if (error) - [userInfo setObject:error forKey:NSUnderlyingErrorKey]; - [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] - cause:cause = UbiquityStoreErrorCauseMigrateToCloudStore context:context = exception]; + [self loadStoreAtURL:cloudStoreURL withOptions:cloudStoreOptions + migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy + error:&error cause:&cause context:&context]; } @finally { self.migrationStoreURL = nil; @@ -589,43 +417,44 @@ - (void)loadLocalStore { // Load local store if iCloud is disabled. __block NSError *error = nil; - NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: - @YES, NSMigratePersistentStoresAutomaticallyOption, - @YES, NSInferMappingModelAutomaticallyOption, - nil]; - [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; // Make sure local store directory exists. - if (![[NSFileManager defaultManager] createDirectoryAtPath:[self URLForLocalStoreDirectory].path + NSURL *localStoreURL = [self URLForLocalStore]; + NSURL *localStoreDirectoryURL = [self URLForLocalStoreDirectory]; + if (![[NSFileManager defaultManager] createDirectoryAtPath:localStoreDirectoryURL.path withIntermediateDirectories:YES attributes:nil error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = [self URLForLocalStoreDirectory].path]; + [self error:error cause:cause = UbiquityStoreErrorCauseCreateStorePath context:context = localStoreDirectoryURL.path]; return; } // If the local store doesn't exist yet and a migrationStore is set, copy it. - NSURL *localStoreURL = [self URLForLocalStore]; - if ([[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path] && - ![[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) { - [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:self.migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 - writingItemAtURL:self.localStoreURL options:NSFileCoordinatorWritingForReplacing - error:&error byAccessor: - ^(NSURL *newReadingURL, NSURL *newWritingURL) { - if (![[NSFileManager defaultManager] copyItemAtURL:newReadingURL toURL:newWritingURL error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseMigrateToLocalStore context:context = self.migrationStoreURL.path]; - }]; - if (cause != UbiquityStoreErrorCauseNoError) - return; - } + // Check if we need to seed the store by migrating another store into it. + UbiquityStoreMigrationStrategy migrationStrategy = self.migrationStrategy; + NSURL *migrationStoreURL = self.migrationStoreURL; + if (![[NSFileManager defaultManager] fileExistsAtPath:self.migrationStoreURL.path] || + [[NSFileManager defaultManager] fileExistsAtPath:localStoreURL.path]) + migrationStrategy = UbiquityStoreMigrationStrategyNone; - // Add local store to PSC. - [self log:@"Loading local store."]; - if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType - configuration:nil URL:localStoreURL - options:localStoreOptions - error:&error]) { - [self error:error cause:cause = UbiquityStoreErrorCauseOpenLocalStore context:context = localStoreURL.path]; - return; - } + // Load the local store. + NSMutableDictionary *localStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSMigratePersistentStoresAutomaticallyOption, + @YES, NSInferMappingModelAutomaticallyOption, + nil]; + NSMutableDictionary *migrationStoreOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys: + @YES, NSReadOnlyPersistentStoreOption, + nil]; + if ([[self.migrationStoreURL URLByDeletingLastPathComponent].path + isEqualToString:[self URLForCloudStoreDirectory].path]) + // Migration store is a cloud store. + [migrationStoreOptions addEntriesFromDictionary:@{ + NSPersistentStoreUbiquitousContentNameKey : self.contentName, + NSPersistentStoreUbiquitousContentURLKey : [self URLForCloudContent], + }]; + [localStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; + [self loadStoreAtURL:localStoreURL withOptions:localStoreOptions + migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy + error:&error cause:&cause context:&context]; } @finally { self.migrationStoreURL = nil; @@ -660,6 +489,188 @@ - (void)loadLocalStore { } } +- (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary *)targetStoreOptions + migratingStoreAtURL:(NSURL *)migrationStoreURL withOptions:(NSMutableDictionary *)migrationStoreOptions + usingStrategy:(UbiquityStoreMigrationStrategy)migrationStrategy + error:(NSError **)error cause:(UbiquityStoreErrorCause *)cause context:(id *)context { + + @try { + switch (migrationStrategy) { + case UbiquityStoreMigrationStrategyCopyEntities: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyCopyEntities"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + // Open migration and target store. + NSPersistentStoreCoordinator *migrationCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + __block NSPersistentStore *migrationStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:error byAccessor:^(NSURL *newURL) { + NSLog(@"Adding store: %@\nOptions: %@", newURL.path, migrationStoreOptions); + migrationStore = [migrationCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:migrationStoreOptions + error:error]; + }]; + if (!migrationStore) { + [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = migrationStoreURL.path]; + break; + } + + NSPersistentStoreCoordinator *targetCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; + __block NSPersistentStore *targetStore = nil; + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:error byAccessor:^(NSURL *newURL) { + NSLog(@"Adding store: %@\nOptions: %@", newURL.path, targetStoreOptions); + targetStore = [targetCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:targetStoreOptions + error:error]; + }]; + if (!targetStore) { + [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + break; + } + + // Set up contexts for them. + NSManagedObjectContext *migrationContext = [NSManagedObjectContext new]; + NSManagedObjectContext *targetContext = [NSManagedObjectContext new]; + migrationContext.persistentStoreCoordinator = migrationCoordinator; + targetContext.persistentStoreCoordinator = targetCoordinator; + + // Migrate metadata. + NSMutableDictionary *metadata = [[migrationCoordinator metadataForPersistentStore:migrationStore] mutableCopy]; + for (NSString *key in [[metadata allKeys] copy]) + if ([key hasPrefix:@"com.apple.coredata.ubiquity"]) + // Don't migrate ubiquitous metadata. + [metadata removeObjectForKey:key]; + [metadata addEntriesFromDictionary:[targetCoordinator metadataForPersistentStore:targetStore]]; + [targetCoordinator setMetadata:metadata forPersistentStore:targetStore]; + + // Migrate entities. + BOOL migrationFailure = NO; + NSMutableDictionary *migratedIDsBySourceID = [[NSMutableDictionary alloc] initWithCapacity:500]; + for (NSEntityDescription *entity in self.model.entities) { + NSFetchRequest *fetch = [NSFetchRequest new]; + fetch.entity = entity; + fetch.fetchBatchSize = 500; + fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; + + NSArray *localObjects = [migrationContext executeFetchRequest:fetch error:error]; + if (!localObjects) { + migrationFailure = YES; + break; + } + + for (NSManagedObject *localObject in localObjects) + [self copyMigrateObject:localObject toContext:targetContext usingMigrationCache:migratedIDsBySourceID]; + } + + // Save migrated entities and unload the stores. + if (!migrationFailure && ![targetContext save:error]) + migrationFailure = YES; + if (![migrationCoordinator removePersistentStore:migrationStore error:error]) + [self error:*error cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = migrationStore]; + if (![targetCoordinator removePersistentStore:targetStore error:error]) + [self error:*error cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = targetStore]; + + // Handle failure by cleaning up the target store. + if (migrationFailure) { + [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self removeItemAtURL:targetStoreURL localOnly:NO]; + break; + } + + // Migration is finished: load the store. + [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions + migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone + error:error cause:cause context:context]; + break; + } + + case UbiquityStoreMigrationStrategyIOS: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyIOS"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 + writingItemAtURL:targetStoreURL options:NSFileCoordinatorWritingForMerging + error:error byAccessor: + ^(NSURL *newReadingURL, NSURL *newWritingURL) { + // Add the store to migrate. + NSLog(@"Adding store: %@\nOptions: %@", newReadingURL.path, migrationStoreOptions); + NSPersistentStore *migrationStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newReadingURL + options:migrationStoreOptions + error:error]; + if (!migrationStore) + [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = migrationStoreURL.path]; + + else { + NSLog(@"Adding store: %@\nOptions: %@", newWritingURL.path, targetStoreOptions); + if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore + toURL:newWritingURL + options:targetStoreOptions + withType:NSSQLiteStoreType + error:error]) + [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + } + }]; + break; + } + + case UbiquityStoreMigrationStrategyManual: { + [self log:@"Seeding store using strategy: UbiquityStoreMigrationStrategyManual"]; + NSAssert(migrationStoreURL, @"Cannot migrate: No migration store specified."); + + // Instruct the delegate to migrate the migration store to the target store. + if (![self.delegate ubiquityStoreManager:self + manuallyMigrateStore:migrationStoreURL withOptions:migrationStoreOptions + toStore:targetStoreURL withOptions:targetStoreOptions error:error]) { + // Handle failure by cleaning up the target store. + [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self removeItemAtURL:targetStoreURL localOnly:NO]; + break; + } + + // Migration is finished: load the target store. + [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions + migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone + error:error cause:cause context:context]; + break; + } + + case UbiquityStoreMigrationStrategyNone: { + [self log:@"Loading store without seeding."]; + NSAssert([self.persistentStoreCoordinator.persistentStores count] == 0, @"PSC should have no stores before trying to load one."); + + // Load the target store. + [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL + options:(NSFileCoordinatorReadingOptions) 0 + error:error byAccessor:^(NSURL *newURL) { + NSLog(@"Adding store: %@\nOptions: %@", newURL.path, targetStoreOptions); + [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + configuration:nil URL:newURL + options:targetStoreOptions + error:error]; + }]; + if (![self.persistentStoreCoordinator.persistentStores count]) + [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + break; + } + } + } + @catch (id exception) { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; + if (exception) + [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; + if (*error) + [userInfo setObject:*error forKey:NSUnderlyingErrorKey]; + [self error:*error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] + cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = exception]; + } +} + - (id)copyMigrateObject:(NSManagedObject *)sourceObject toContext:(NSManagedObjectContext *)destinationContext usingMigrationCache:(NSMutableDictionary *)migratedIDsBySourceID { if (!sourceObject) @@ -703,7 +714,10 @@ - (id)copyMigrateObject:(NSManagedObject *)sourceObject toContext:(NSManagedObje - (BOOL)cloudSafeForSeeding { - if ([[NSUbiquitousKeyValueStore defaultStore] objectForKey:StoreUUIDKey]) + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + + if ([cloud objectForKey:StoreUUIDKey]) // Migration is only safe when there is no storeUUID yet (the store is not in the cloud yet). return NO; @@ -843,7 +857,7 @@ - (void)rebuildCloudContentFromCloudStore { success = [[NSFileManager defaultManager] moveItemAtURL:newReadingURL toURL:newWritingURL error :&error]; }]; if (!success) { - [self error:error cause:UbiquityStoreErrorCauseMigrateToCloudStore context:self.migrationStoreURL.path]; + [self error:error cause:UbiquityStoreErrorCauseSeedStore context:self.migrationStoreURL.path]; return; } } @@ -875,6 +889,7 @@ - (void)setCloudEnabled:(BOOL)enabled { - (NSString *)storeUUID { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; NSString *storeUUID = [cloud objectForKey:StoreUUIDKey]; // If no storeUUID is set yet, create a new storeUUID and return that as long as no storeUUID is set yet. @@ -958,10 +973,17 @@ - (void)applicationWillTerminate:(NSNotification *)note { - (void)keyValueStoreChanged:(NSNotification *)note { + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud synchronize]; + NSArray *changedKeys = (NSArray *)[note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey]; - if ([changedKeys containsObject:StoreUUIDKey]) + if ([changedKeys containsObject:StoreUUIDKey]) { // The UUID of the active store changed. We need to switch to the newly activated store. + [self log:@"StoreUUID changed (reason: %@) to: %@", + [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey], + [cloud objectForKey:StoreUUIDKey]]; [self cloudStoreChanged:nil]; + } if ([changedKeys containsObject:StoreCorruptedKey]) // Cloud content corruption was detected or cleared. @@ -980,11 +1002,10 @@ - (void)keyValueStoreChanged:(NSNotification *)note { - (BOOL)checkCloudContentCorruption:(NSNotification *)note { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - if (![cloud boolForKey:StoreCorruptedKey]) { - // [self loadStore]; - + [cloud synchronize]; + + if (![cloud boolForKey:StoreCorruptedKey]) return NO; - } // Cloud content corruption detected. if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:handleCloudContentCorruptionIsCloud:)]) { @@ -1020,7 +1041,8 @@ - (void)cloudStoreChanged:(NSNotification *)note { return; // Reload the store. - [self log:@"Cloud store changed. StoreUUID: %@, Identity: %@", self.storeUUID, self.currentIdentityToken]; + [self log:@"Cloud store changed. StoreUUID: %@ (%@), Identity: %@", + self.storeUUID, _tentativeStoreUUID ? @"tentative" : @"definite", self.currentIdentityToken]; [self loadStore]; } From 3bfc6dd5a96142b50122d0e60695b0da41065034 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Sun, 17 Mar 2013 23:51:31 -0400 Subject: [PATCH 28/35] Fixes to deletion of cloud store and failure/corruption handling. [IMPROVED] Don't override behaviour by implementing -ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud: [IMPROVED] Synchronize all persistence methods by moving them onto the persistence queue. [IMPROVED] Post UbiquityManagedStoreDidChangeNotification after removing the persistence store from the coordinator. [FIXED] Issue where another device might recreate a cloud store before we get to after deleting the store. [FIXED] Tentative StoreUUID overrides KVS now to fix the above by not unsetting StoreUUID when deleting the store. [ADDED] Attempt to recover the cloud store after failing to load it by downloading and reimporting it from the cloud content. [ADDED] When the cloud store can't be opened, mark it as corrupted to avoid permanently locking the user out of iCloud. [FIXED] NSErrors were in some cases not logged. --- iCloudStoreManager/UbiquityStoreManager.h | 28 +- iCloudStoreManager/UbiquityStoreManager.m | 325 +++++++++++------- .../project.pbxproj | 1 + iCloudStoreManagerExample/AppDelegate.m | 6 +- 4 files changed, 223 insertions(+), 137 deletions(-) diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 94101c2..7f9938a 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -96,7 +96,7 @@ typedef enum { /** Triggered when the store manager loads a persistence store. * - * This is where you'll init/update your application's persistence layer. + * The manager is done handling the attempt to load the store. This is where you'll init/update your application's persistence layer. * You should probably create your main managed object context here. * * Note the coordinator could change during the application's lifetime (you'll get a new -ubiquityStoreManager:didLoadStoreForCoordinator:isCloud: if this happens). @@ -107,6 +107,19 @@ typedef enum { @required - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore; +/** Triggered when the store manager fails to loads a persistence store. + * + * The manager is done handling the attempt to load the store. The store will be unavailable unless another attempt is made to load the store. + * + * -ubiquityStoreManager:handleCloudContentCorruptionIsCloud: may get called later if the store that failed to load was the cloud store. + * + * @param wasCloudStore YES if the error was caused while attempting to load the cloud store. + * NO if the error was caused while attempting to load the local store. + */ +@optional +- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause + context:(id)context wasCloud:(BOOL)wasCloudStore; + /** Triggered when the store manager has detected that the cloud content has failed to import on one of the devices. * * The result is that the cloud store on this device is no longer guaranteed to be the same as the cloud store on @@ -149,19 +162,6 @@ typedef enum { @optional - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionIsCloud:(BOOL)isCloudStore; -/** Triggered when the store manager fails to loads a persistence store. - * - * Useful to decide what to do to make a store available to the application. - * - * If you don't implement this method, the manager will disable the cloud store and fall back to the local store when loading the cloud store fails. It's the equivalent to implementing this method with `manager.cloudEnabled = NO;`. - * - * @param wasCloudStore YES if the error was caused while attempting to load the cloud store. - * NO if the error was caused while attempting to load the local store. - */ -@optional -- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreWithCause:(UbiquityStoreErrorCause)cause - context:(id)context wasCloud:(BOOL)wasCloudStore; - /** Triggered when the store manager encounters an error. Mainly useful to handle error conditions/logging in whatever way you see fit. * * If you don't implement this method, the manager will instead detail the error in a few log statements. diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index fafa1b4..1ea3b8c 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -39,6 +39,7 @@ @interface UbiquityStoreManager () @property (nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; @property (nonatomic, strong) id currentIdentityToken; @property (nonatomic, strong) NSURL *migrationStoreURL; +@property(nonatomic) BOOL attemptingCloudRecovery; @end @@ -59,8 +60,6 @@ + (void)initialize { withMethod:@selector(init_USM_WithDomain:code:userInfo:) error:&error]) NSLog(@"UbiquityStoreManager: Warning: Failed to swizzle, won't be able to detect desync issues. Cause: %@", error); - else - NSLog(@"UbiquityStoreManager: Swizzled (from %@).", self.class); } - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL @@ -90,7 +89,6 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb _persistentStorageQueue.maxConcurrentOperationCount = 1; _presentedItemOperationQueue = [NSOperationQueue new]; _presentedItemOperationQueue.name = [NSString stringWithFormat:@"%@PresenterQueue", NSStringFromClass([self class])]; - _presentedItemOperationQueue.maxConcurrentOperationCount = 1; // Observe application events. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyValueStoreChanged:) @@ -130,9 +128,11 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb - (void)dealloc { [NSFileCoordinator removeFilePresenter:self]; - [self.persistentStoreCoordinator tryLock]; - [self clearStore]; - [self.persistentStoreCoordinator unlock]; + [self.persistentStorageQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ + [self.persistentStoreCoordinator tryLock]; + [self clearStore]; + [self.persistentStoreCoordinator unlock]; + }]] waitUntilFinished:YES]; } #pragma mark - File Handling @@ -237,6 +237,9 @@ - (void)error:(NSError *)error cause:(UbiquityStoreErrorCause)cause context:(id) - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Persistence coordinator should only be accessed from the persistence queue."); + if (!_persistentStoreCoordinator) { _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChanges:) @@ -249,6 +252,9 @@ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - (void)resetPersistentStoreCoordinator { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Persistence coordinator should only be modified from the persistence queue."); + BOOL wasLocked = NO; if (_persistentStoreCoordinator) { wasLocked = ![_persistentStoreCoordinator tryLock]; @@ -262,15 +268,13 @@ - (void)resetPersistentStoreCoordinator { - (void)clearStore { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Store should only be cleared from the persistence queue."); + [self log:@"Clearing stores..."]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) [self.delegate ubiquityStoreManager:self willLoadStoreIsCloud:self.cloudEnabled]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification - object:self userInfo:nil]; - }); - // Remove the store from the coordinator. NSError *error = nil; for (NSPersistentStore *store in self.persistentStoreCoordinator.persistentStores) @@ -280,6 +284,11 @@ - (void)clearStore { if ([self.persistentStoreCoordinator.persistentStores count]) // We couldn't remove all the stores, make a new PSC instead. [self resetPersistentStoreCoordinator]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification + object:self userInfo:nil]; + }); } - (void)loadStore { @@ -303,17 +312,20 @@ - (void)loadStore { - (void)loadCloudStore { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Active store should only be changed from the persistence queue."); + [self log:@"Will load cloud store: %@ (%@).", self.storeUUID, _tentativeStoreUUID? @"tentative": @"definite"]; - // Check if the cloud store has been locked down because of content corruption. - if ([self checkCloudContentCorruption:nil]) + // If the store is not tentative, check if the cloud store has been locked down because of content corruption. + if (!self.tentativeStoreUUID && [self checkCloudContentCorruption:nil]) // We don't put this in the @try block because we don't want to handle this failure in the @finally block. - // That's because the check method allows the application to take action which would confuse the @finally block. + // That's because -checkCloudContentCorruption has application hooks that might confuse the @finally block. return; - __block id context = nil; - __block NSError *error = nil; - __block UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + id context = nil; + NSError *error = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; @@ -341,9 +353,7 @@ - (void)loadCloudStore { // We have a cloud store but no cloud content. The cloud content was deleted: // The existing store cannot sync anymore and needs to be recreated. [self log:@"Deleting cloud store: it has no cloud content."]; - - if (![[NSFileManager defaultManager] removeItemAtURL:cloudStoreURL error:&error]) - [self error:error cause:cause = UbiquityStoreErrorCauseDeleteStore context:context = cloudStoreURL.path]; + [self removeItemAtURL:cloudStoreURL localOnly:NO]; } // Check if we need to seed the store by migrating another store into it. @@ -366,7 +376,7 @@ - (void)loadCloudStore { [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; [self loadStoreAtURL:cloudStoreURL withOptions:cloudStoreOptions migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy - error:&error cause:&cause context:&context]; + cause:&cause context:&context]; } @finally { self.migrationStoreURL = nil; @@ -378,46 +388,57 @@ - (void)loadCloudStore { } else { // An error occurred in the @try block. - [self resetTentativeStoreUUID]; + [self unsetTentativeStoreUUID]; [self clearStore]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { - [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Application will handle.", cause, context]; - } else { - [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Handling with default strategy: falling back to local store.", cause, context]; + // If we haven't attempted recovery yet (ie. delete the local store), try that first. + if (!self.attemptingCloudRecovery) { + self.attemptingCloudRecovery = YES; + [self deleteCloudStoreLocalOnly:YES]; + return; } + + // Failed to load regardless of recovery attempt. Mark store as corrupt. + [self log:@"Cloud enabled but failed to load cloud store (cause:%u, %@). Marking cloud store as corrupt. Store will be unavailable.", cause, context]; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; + [cloud setBool:YES forKey:StoreCorruptedKey]; + [cloud synchronize]; } + NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; dispatch_async(dispatch_get_main_queue(), ^{ if (cause == UbiquityStoreErrorCauseNoError) { // Store loaded successfully. - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) - [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:YES]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) { + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:psc isCloud:YES]; + } [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; - } else if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + } else { // Store failed to load, inform delegate. - [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; - else - // Store failed to load, delegate doesn't care. Default strategy for cloud load failure: switch to local. - self.cloudEnabled = NO; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) + [self.delegate ubiquityStoreManager:self failedLoadingStoreWithCause:cause context:context wasCloud:YES]; + + [self checkCloudContentCorruption:nil]; + } }); } } - (void)loadLocalStore { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Active store should only be changed from the persistence queue."); + [self log:@"Will load local store."]; - __block id context = nil; - __block UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; + id context = nil; + NSError *error = nil; + UbiquityStoreErrorCause cause = UbiquityStoreErrorCauseNoError; @try { [self clearStore]; - // Load local store if iCloud is disabled. - __block NSError *error = nil; - // Make sure local store directory exists. NSURL *localStoreURL = [self URLForLocalStore]; NSURL *localStoreDirectoryURL = [self URLForLocalStoreDirectory]; @@ -454,7 +475,7 @@ - (void)loadLocalStore { [migrationStoreOptions addEntriesFromDictionary:self.additionalStoreOptions]; [self loadStoreAtURL:localStoreURL withOptions:localStoreOptions migratingStoreAtURL:migrationStoreURL withOptions:migrationStoreOptions usingStrategy:migrationStrategy - error:&error cause:&cause context:&context]; + cause:&cause context:&context]; } @finally { self.migrationStoreURL = nil; @@ -467,18 +488,16 @@ - (void)loadLocalStore { // An error occurred in the @try block. [self clearStore]; - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:failedLoadingStoreWithCause:context:wasCloud:)]) { - [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Application will handle.", cause, context]; - } else { - [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Handling with default strategy: no store available.", cause, context]; - } + [self log:@"Cloud disabled but failed to load local store (cause:%u, %@). Store will be unavailable.", cause, context]; } + NSPersistentStoreCoordinator *psc = self.persistentStoreCoordinator; dispatch_async(dispatch_get_main_queue(), ^{ if (cause == UbiquityStoreErrorCauseNoError) { // Store loaded successfully. - if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) - [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:self.persistentStoreCoordinator isCloud:NO]; + if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:)]) { + [self.delegate ubiquityStoreManager:self didLoadStoreForCoordinator:psc isCloud:NO]; + } [[NSNotificationCenter defaultCenter] postNotificationName:UbiquityManagedStoreDidChangeNotification object:self userInfo:nil]; @@ -492,8 +511,13 @@ - (void)loadLocalStore { - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary *)targetStoreOptions migratingStoreAtURL:(NSURL *)migrationStoreURL withOptions:(NSMutableDictionary *)migrationStoreOptions usingStrategy:(UbiquityStoreMigrationStrategy)migrationStrategy - error:(NSError **)error cause:(UbiquityStoreErrorCause *)cause context:(id *)context { + cause:(UbiquityStoreErrorCause *)cause context:(id *)context { + + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Active store should only be changed from the persistence queue."); + NSError *error = nil; + __block NSError *error_ = nil; @try { switch (migrationStrategy) { case UbiquityStoreMigrationStrategyCopyEntities: { @@ -505,15 +529,14 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary __block NSPersistentStore *migrationStore = nil; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 - error:error byAccessor:^(NSURL *newURL) { - NSLog(@"Adding store: %@\nOptions: %@", newURL.path, migrationStoreOptions); + error:&error byAccessor:^(NSURL *newURL) { migrationStore = [migrationCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newURL options:migrationStoreOptions - error:error]; + error:&error_]; }]; if (!migrationStore) { - [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = migrationStoreURL.path]; + [self error:error_? error_: error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = migrationStoreURL.path]; break; } @@ -521,15 +544,14 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary __block NSPersistentStore *targetStore = nil; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL options:(NSFileCoordinatorReadingOptions) 0 - error:error byAccessor:^(NSURL *newURL) { - NSLog(@"Adding store: %@\nOptions: %@", newURL.path, targetStoreOptions); + error:&error byAccessor:^(NSURL *newURL) { targetStore = [targetCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newURL options:targetStoreOptions - error:error]; + error:&error_]; }]; if (!targetStore) { - [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + [self error:error_ ? error_ : error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; break; } @@ -557,7 +579,7 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary fetch.fetchBatchSize = 500; fetch.relationshipKeyPathsForPrefetching = entity.relationshipsByName.allKeys; - NSArray *localObjects = [migrationContext executeFetchRequest:fetch error:error]; + NSArray *localObjects = [migrationContext executeFetchRequest:fetch error:&error]; if (!localObjects) { migrationFailure = YES; break; @@ -568,16 +590,16 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary } // Save migrated entities and unload the stores. - if (!migrationFailure && ![targetContext save:error]) + if (!migrationFailure && ![targetContext save:&error]) migrationFailure = YES; - if (![migrationCoordinator removePersistentStore:migrationStore error:error]) - [self error:*error cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = migrationStore]; - if (![targetCoordinator removePersistentStore:targetStore error:error]) - [self error:*error cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = targetStore]; + if (![migrationCoordinator removePersistentStore:migrationStore error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = migrationStore]; + if (![targetCoordinator removePersistentStore:targetStore error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseClearStore context:*context = targetStore]; // Handle failure by cleaning up the target store. if (migrationFailure) { - [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self error:error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; [self removeItemAtURL:targetStoreURL localOnly:NO]; break; } @@ -585,7 +607,7 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary // Migration is finished: load the store. [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone - error:error cause:cause context:context]; + cause:cause context:context]; break; } @@ -595,27 +617,27 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:migrationStoreURL options:(NSFileCoordinatorReadingOptions) 0 writingItemAtURL:targetStoreURL options:NSFileCoordinatorWritingForMerging - error:error byAccessor: + error:&error byAccessor: ^(NSURL *newReadingURL, NSURL *newWritingURL) { // Add the store to migrate. - NSLog(@"Adding store: %@\nOptions: %@", newReadingURL.path, migrationStoreOptions); NSPersistentStore *migrationStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newReadingURL options:migrationStoreOptions - error:error]; + error:&error_]; if (!migrationStore) - [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = migrationStoreURL.path]; + [self error:error_ cause:*cause = UbiquityStoreErrorCauseOpenSeedStore context:*context = newReadingURL.path]; - else { - NSLog(@"Adding store: %@\nOptions: %@", newWritingURL.path, targetStoreOptions); - if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore + else if (![self.persistentStoreCoordinator migratePersistentStore:migrationStore toURL:newWritingURL options:targetStoreOptions withType:NSSQLiteStoreType - error:error]) - [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; - } + error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = newWritingURL.path]; + else + *cause = UbiquityStoreErrorCauseNoError; }]; + if (error) + [self error:error cause:UbiquityStoreErrorCauseOpenSeedStore context:migrationStoreURL.path]; break; } @@ -626,9 +648,9 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary // Instruct the delegate to migrate the migration store to the target store. if (![self.delegate ubiquityStoreManager:self manuallyMigrateStore:migrationStoreURL withOptions:migrationStoreOptions - toStore:targetStoreURL withOptions:targetStoreOptions error:error]) { + toStore:targetStoreURL withOptions:targetStoreOptions error:&error]) { // Handle failure by cleaning up the target store. - [self error:*error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; + [self error:error cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = migrationStoreURL.path]; [self removeItemAtURL:targetStoreURL localOnly:NO]; break; } @@ -636,7 +658,7 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary // Migration is finished: load the target store. [self loadStoreAtURL:targetStoreURL withOptions:targetStoreOptions migratingStoreAtURL:nil withOptions:nil usingStrategy:UbiquityStoreMigrationStrategyNone - error:error cause:cause context:context]; + cause:cause context:context]; break; } @@ -647,15 +669,19 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary // Load the target store. [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:targetStoreURL options:(NSFileCoordinatorReadingOptions) 0 - error:error byAccessor:^(NSURL *newURL) { - NSLog(@"Adding store: %@\nOptions: %@", newURL.path, targetStoreOptions); - [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType + error:&error byAccessor:^(NSURL *newURL) { + if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:newURL options:targetStoreOptions - error:error]; + error:&error_]) + [self error:error_ cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = newURL.path]; }]; - if (![self.persistentStoreCoordinator.persistentStores count]) - [self error:*error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + + if (error) + [self error:error cause:*cause = UbiquityStoreErrorCauseOpenActiveStore context:*context = targetStoreURL.path]; + if ([self.persistentStoreCoordinator.persistentStores count]) + *cause = UbiquityStoreErrorCauseNoError; + break; } } @@ -664,15 +690,21 @@ - (void)loadStoreAtURL:(NSURL *)targetStoreURL withOptions:(NSMutableDictionary NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2]; if (exception) [userInfo setObject:[exception description] forKey:NSLocalizedFailureReasonErrorKey]; - if (*error) - [userInfo setObject:*error forKey:NSUnderlyingErrorKey]; - [self error:*error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] + if (error_) + [userInfo setObject:error_ forKey:NSUnderlyingErrorKey]; + else if (error) + [userInfo setObject:error forKey:NSUnderlyingErrorKey]; + + [self error:[NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo] cause:*cause = UbiquityStoreErrorCauseSeedStore context:*context = exception]; } } - (id)copyMigrateObject:(NSManagedObject *)sourceObject toContext:(NSManagedObjectContext *)destinationContext usingMigrationCache:(NSMutableDictionary *)migratedIDsBySourceID { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Migration should only be done from the persistence queue."); + if (!sourceObject) return nil; @@ -717,8 +749,8 @@ - (BOOL)cloudSafeForSeeding { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud synchronize]; - if ([cloud objectForKey:StoreUUIDKey]) - // Migration is only safe when there is no storeUUID yet (the store is not in the cloud yet). + if (!self.tentativeStoreUUID && [cloud objectForKey:StoreUUIDKey]) + // Migration is only safe when the target is a new store (tentative or no StoreUUID). return NO; if ([[NSFileManager defaultManager] fileExistsAtPath:[self URLForCloudStore].path]) @@ -730,11 +762,15 @@ - (BOOL)cloudSafeForSeeding { - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { + // The file coordination below fails without an error, when the file at directoryURL doesn't exist. We ignore this. NSError *error = nil; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateWritingItemAtURL:directoryURL options:NSFileCoordinatorWritingForDeleting error:&error byAccessor: ^(NSURL *newURL) { + if (![[NSFileManager defaultManager] fileExistsAtPath:newURL.path]) + return; + NSError *error_ = nil; if (localOnly && [[NSFileManager defaultManager] isUbiquitousItemAtURL:newURL]) { if (![[NSFileManager defaultManager] evictUbiquitousItemAtURL:newURL error:&error_]) @@ -744,6 +780,7 @@ - (void)removeItemAtURL:(NSURL *)directoryURL localOnly:(BOOL)localOnly { [self error:error_ cause:UbiquityStoreErrorCauseDeleteStore context:newURL.path]; } }]; + if (error) [self error:error cause:UbiquityStoreErrorCauseDeleteStore context:directoryURL.path]; } @@ -760,13 +797,13 @@ - (void)deleteCloudContainerLocalOnly:(BOOL)localOnly { [self removeItemAtURL:[self URLForCloudContainer] localOnly:localOnly]; // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; if (!localOnly) { + [self createTentativeStoreUUID]; NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud synchronize]; for (id key in [[cloud dictionaryRepresentation] allKeys]) [cloud removeObjectForKey:key]; - [cloud synchronize]; + // Don't synchronize. Otherwise another devices might recreate the cloud store before we do. } if (self.cloudEnabled) @@ -786,14 +823,9 @@ - (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self removeItemAtURL:[self URLForCloudStore] localOnly:localOnly]; [self removeItemAtURL:[self URLForCloudContent] localOnly:localOnly]; - // Unset the storeUUID so a new one will be created. - [self resetTentativeStoreUUID]; - if (!localOnly) { - NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; - [cloud removeObjectForKey:StoreCorruptedKey]; - [cloud removeObjectForKey:StoreUUIDKey]; - [cloud synchronize]; - } + // Create a tentative StoreUUID so a new cloud store will be created. + if (!localOnly) + [self createTentativeStoreUUID]; if (self.cloudEnabled) [self loadStore]; @@ -847,17 +879,19 @@ - (void)rebuildCloudContentFromCloudStore { [self log:@"Cannot rebuild cloud content: Cloud store doesn't exist."]; } else { + NSError *error = nil; + __block NSError *error_ = nil; __block BOOL success = NO; - __block NSError *error = nil; self.migrationStoreURL = [[self URLForCloudStoreDirectory] URLByAppendingPathComponent:CloudStoreMigrationSource isDirectory:NO]; [[[NSFileCoordinator alloc] initWithFilePresenter:nil] coordinateReadingItemAtURL:cloudStoreURL options:(NSFileCoordinatorReadingOptions) 0 writingItemAtURL:self.migrationStoreURL options:NSFileCoordinatorWritingForReplacing error:&error byAccessor: ^(NSURL *newReadingURL, NSURL *newWritingURL) { - success = [[NSFileManager defaultManager] moveItemAtURL:newReadingURL toURL:newWritingURL error :&error]; + [[NSFileManager defaultManager] removeItemAtURL:newWritingURL error:&error_]; + success = [[NSFileManager defaultManager] moveItemAtURL:newReadingURL toURL:newWritingURL error :&error_]; }]; if (!success) { - [self error:error cause:UbiquityStoreErrorCauseSeedStore context:self.migrationStoreURL.path]; + [self error:error_? error_: error cause:UbiquityStoreErrorCauseSeedStore context:self.migrationStoreURL.path]; return; } } @@ -881,48 +915,74 @@ - (void)setCloudEnabled:(BOOL)enabled { // No change, do nothing to avoid a needless store reload. return; - NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; - [local setBool:enabled forKey:CloudEnabledKey]; - [self loadStore]; + [self.persistentStorageQueue addOperationWithBlock:^{ + [NSFileCoordinator removeFilePresenter:self]; + NSUserDefaults *local = [NSUserDefaults standardUserDefaults]; + [local setBool:enabled forKey:CloudEnabledKey]; + [NSFileCoordinator addFilePresenter:self]; + + [self loadStore]; + }]; } - (NSString *)storeUUID { + if (self.tentativeStoreUUID) + // A tentative StoreUUID is set; this means a new cloud store is being created. + return self.tentativeStoreUUID; + NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud synchronize]; NSString *storeUUID = [cloud objectForKey:StoreUUIDKey]; - // If no storeUUID is set yet, create a new storeUUID and return that as long as no storeUUID is set yet. - // When the migration to the new storeUUID is successful, we update the iCloud's KVS with a call to -setStoreUUID. - if (!storeUUID) { - if (!self.tentativeStoreUUID) - self.tentativeStoreUUID = [[NSUUID UUID] UUIDString]; - storeUUID = self.tentativeStoreUUID; - } + if (!storeUUID) + // No StoreUUID is set; this means there is no cloud store yet. Set a new tentative StoreUUID to create one. + return [self createTentativeStoreUUID]; return storeUUID; } /** - * When a tentativeStoreUUID is set, this operation confirms it and writes it as the new storeUUID to the iCloud KVS. + * When a tentative StoreUUID is set, this operation confirms it and writes it as the new StoreUUID to the iCloud KVS. */ - (void)confirmTentativeStoreUUID { + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Tentative StoreUUID should only be confirmed from the persistence queue."); + if (self.tentativeStoreUUID) { NSUbiquitousKeyValueStore *cloud = [NSUbiquitousKeyValueStore defaultStore]; [cloud setObject:self.tentativeStoreUUID forKey:StoreUUIDKey]; + [cloud removeObjectForKey:StoreCorruptedKey]; [cloud synchronize]; - [self resetTentativeStoreUUID]; + [self unsetTentativeStoreUUID]; } } /** - * When a tentativeStoreUUID is set, this operation resets it so that a new one will be generated if necessary. + * Creates a new a tentative StoreUUID. This will result in a new cloud store being created. */ -- (void)resetTentativeStoreUUID { +- (NSString *)createTentativeStoreUUID { + [NSFileCoordinator removeFilePresenter:self]; + self.tentativeStoreUUID = [[NSUUID UUID] UUIDString]; + [NSFileCoordinator addFilePresenter:self]; + + return self.tentativeStoreUUID; +} + +/** + * Creates a new a tentative StoreUUID. This will result in a new cloud store being created. + */ +- (void)unsetTentativeStoreUUID { + + NSAssert([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue], + @"Tentative StoreUUID should only be unset from the persistence queue."); + + [NSFileCoordinator removeFilePresenter:self]; self.tentativeStoreUUID = nil; + [NSFileCoordinator addFilePresenter:self]; } #pragma mark - NSFilePresenter @@ -942,11 +1002,15 @@ -(NSOperationQueue *)presentedItemOperationQueue { - (void)accommodatePresentedItemDeletionWithCompletionHandler:(void (^)(NSError *))completionHandler { - [self clearStore]; + NSURL *activeStoreURL = [((NSPersistentStore *)[_persistentStoreCoordinator.persistentStores lastObject]) URL]; + if ([activeStoreURL isEqual:self.presentedItemURL]) + [self.persistentStorageQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ + [self clearStore]; + }]] waitUntilFinished:YES]; + completionHandler(nil); } - #pragma mark - Notifications - (void)applicationDidBecomeActive:(NSNotification *)note { @@ -963,12 +1027,16 @@ - (void)applicationWillEnterForeground:(NSNotification *)note { - (void)applicationDidEnterBackground:(NSNotification *)note { - [self clearStore]; + [self.persistentStorageQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ + [self clearStore]; + }]] waitUntilFinished:YES]; } - (void)applicationWillTerminate:(NSNotification *)note { - [self clearStore]; + [self.persistentStorageQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ + [self clearStore]; + }]] waitUntilFinished:YES]; } - (void)keyValueStoreChanged:(NSNotification *)note { @@ -982,13 +1050,20 @@ - (void)keyValueStoreChanged:(NSNotification *)note { [self log:@"StoreUUID changed (reason: %@) to: %@", [note.userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey], [cloud objectForKey:StoreUUIDKey]]; - [self cloudStoreChanged:nil]; + + [self.persistentStorageQueue addOperationWithBlock:^{ + [NSFileCoordinator removeFilePresenter:self]; + [self unsetTentativeStoreUUID]; + [NSFileCoordinator addFilePresenter:self]; + + [self cloudStoreChanged:nil]; + }]; } if ([changedKeys containsObject:StoreCorruptedKey]) // Cloud content corruption was detected or cleared. if (![self checkCloudContentCorruption:nil]) - if (self.cloudEnabled && ![self.persistentStoreCoordinator.persistentStores count]) + if (self.cloudEnabled && ![_persistentStoreCoordinator.persistentStores count]) // Corruption was removed and our cloud store is not yet loaded. Try loading the store again. [self loadStore]; } @@ -1012,10 +1087,16 @@ - (BOOL)checkCloudContentCorruption:(NSNotification *)note { [self log:@"Cloud content corruption detected. Application will handle."]; [self.delegate ubiquityStoreManager:self handleCloudContentCorruptionIsCloud:self.cloudEnabled]; - if (self.cloudEnabled) + if (self.cloudEnabled) { // Since the cloud content is corrupt, we must unload the cloud store to prevent // unsyncable changes from being made. - [self clearStore]; + if ([[NSOperationQueue currentQueue] isEqual:self.persistentStorageQueue]) + [self clearStore]; + else + [self.persistentStorageQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{ + [self clearStore]; + }]] waitUntilFinished:YES]; + } } else { // Default strategy for corrupt cloud content: switch to local. [self log:@"Cloud content corruption detected. Handling with default strategy: Falling back to local store."]; diff --git a/iCloudStoreManagerExample.xcodeproj/project.pbxproj b/iCloudStoreManagerExample.xcodeproj/project.pbxproj index 132776c..331d7ef 100644 --- a/iCloudStoreManagerExample.xcodeproj/project.pbxproj +++ b/iCloudStoreManagerExample.xcodeproj/project.pbxproj @@ -466,6 +466,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "iCloudStoreManagerExample/iCloudStoreManagerExample-Prefix.pch"; INFOPLIST_FILE = "iCloudStoreManagerExample/iCloudStoreManagerExample-Info.plist"; + ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; }; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index c3acadf..d23070e 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -36,6 +36,8 @@ + (AppDelegate *)appDelegate { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + NSLog(@"Starting iCloudStoreManagerExample on device: %@\n\n", [UIDevice currentDevice].name); + // STEP 1 - Initialize the UbiquityStoreManager ubiquityStoreManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:[self managedObjectModel] localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil @@ -192,7 +194,6 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager failedLoadingStoreW dispatch_async(dispatch_get_main_queue(), ^{ [masterViewController.storeLoadingActivity stopAnimating]; }); - manager.cloudEnabled = NO; } - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore { @@ -214,6 +215,9 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoor - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionIsCloud:(BOOL)isCloudStore { + if ([self.handleCloudContentAlert isVisible]) + NSLog(@"already showing."); + dispatch_async(dispatch_get_main_queue(), ^{ self.handleCloudContentAlert = [[UIAlertView alloc] initWithTitle:@"Problem With iCloud Sync!" message: @"An error has occurred within Apple's iCloud causing your devices to no longer " From 1db9a7e57c6b72dbe6d5d6f6394ac174737cd0e1 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 18 Mar 2013 23:40:39 -0400 Subject: [PATCH 29/35] Auto-correct iCloud sync from working device. [UPDATED] Example UI now shows a much more friendly alert on cloud desync that just waits for the problem to be automatically fixed and offers a manual override option. --- ...nager.h => NSError+UbiquityStoreManager.h} | 0 ...nager.m => NSError+UbiquityStoreManager.m} | 2 +- iCloudStoreManager/UbiquityStoreManager.h | 19 ++++- iCloudStoreManager/UbiquityStoreManager.m | 22 +++--- .../project.pbxproj | 12 +-- iCloudStoreManagerExample/AppDelegate.m | 75 ++++++++++--------- 6 files changed, 73 insertions(+), 57 deletions(-) rename iCloudStoreManager/{NSManagedObject+UbiquityStoreManager.h => NSError+UbiquityStoreManager.h} (100%) rename iCloudStoreManager/{NSManagedObject+UbiquityStoreManager.m => NSError+UbiquityStoreManager.m} (95%) diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h b/iCloudStoreManager/NSError+UbiquityStoreManager.h similarity index 100% rename from iCloudStoreManager/NSManagedObject+UbiquityStoreManager.h rename to iCloudStoreManager/NSError+UbiquityStoreManager.h diff --git a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m b/iCloudStoreManager/NSError+UbiquityStoreManager.m similarity index 95% rename from iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m rename to iCloudStoreManager/NSError+UbiquityStoreManager.m index 445552f..1adb2e3 100644 --- a/iCloudStoreManager/NSManagedObject+UbiquityStoreManager.m +++ b/iCloudStoreManager/NSError+UbiquityStoreManager.m @@ -5,7 +5,7 @@ // -#import "NSManagedObject+UbiquityStoreManager.h" +#import "NSError+UbiquityStoreManager.h" NSString *const UbiquityManagedStoreDidDetectCorruptionNotification = @"UbiquityManagedStoreDidDetectCorruptionNotification"; NSString *const StoreCorruptedKey = @"USMStoreCorruptedKey"; // cloud: Set to YES when a cloud content corruption has been detected. diff --git a/iCloudStoreManager/UbiquityStoreManager.h b/iCloudStoreManager/UbiquityStoreManager.h index 7f9938a..cdb4ce9 100644 --- a/iCloudStoreManager/UbiquityStoreManager.h +++ b/iCloudStoreManager/UbiquityStoreManager.h @@ -23,7 +23,7 @@ // // Known issues: // - Sometimes Apple's iCloud implementation hangs itself coordinating access for importing ubiquitous changes. -// - Reloading the store with -loadStore can sometimes cause these changes to get imported. +// - Reloading the store with -reloadStore can sometimes cause these changes to get imported. // - If not, the app needs to be restarted. // - Sometimes Apple's iCloud implementation will write corrupting transaction logs to the cloud container. // - As a result, all other devices will fail to import any future changes to the store. @@ -142,9 +142,9 @@ typedef enum { * When you receive this method, there are a few things you can do to handle the situation: * - Switch to the local store (manager.cloudEnabled = NO). * NOTE: The cloud data and cloud syncing will be unavailable. - * - Delete the cloud store and recreate it by seeding it with the local store ([manager deleteCloudStoreLocalOnly:NO]). - * NOTE: The existing cloud store will be lost. - * - Keep the existing cloud data but disable iCloud ([manager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]). + * - Delete the cloud data and recreate it by seeding it with the local store ([manager deleteCloudStoreLocalOnly:NO]). + * NOTE: The existing cloud data will be lost. + * - Make the existing cloud data local and disable iCloud ([manager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]). * NOTE: The existing local store will be lost. * NOTE: You should set localOnly to NO so that the corruption is cleared and enabling iCloud in the future * will seed it with the new local store. @@ -214,6 +214,15 @@ typedef enum { - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedObjectModel *)model localStoreURL:(NSURL *)localStoreURL containerIdentifier:(NSString *)containerIdentifier additionalStoreOptions:(NSDictionary *)additionalStoreOptions delegate:(id)delegate; +#pragma mark - Store Management + +/** + * Clear and re-open the store. + * + * This is rarely useful if you want to re-try opening the active store. You usually won't need to invoke this manually. + */ +- (void)reloadStore; + /** * This will delete all the data from iCloud for this application. * @@ -251,6 +260,8 @@ typedef enum { */ - (void)rebuildCloudContentFromCloudStore; +#pragma mark - Store Information + /** * Determine whether it's safe to seed the cloud store with a local store. */ diff --git a/iCloudStoreManager/UbiquityStoreManager.m b/iCloudStoreManager/UbiquityStoreManager.m index 1ea3b8c..1857d9c 100644 --- a/iCloudStoreManager/UbiquityStoreManager.m +++ b/iCloudStoreManager/UbiquityStoreManager.m @@ -8,7 +8,7 @@ #import "UbiquityStoreManager.h" #import "JRSwizzle.h" -#import "NSManagedObject+UbiquityStoreManager.h" +#import "NSError+UbiquityStoreManager.h" #if TARGET_OS_IPHONE #import @@ -120,7 +120,7 @@ - (id)initStoreNamed:(NSString *)contentName withManagedObjectModel:(NSManagedOb #endif [NSFileCoordinator addFilePresenter:self]; - [self loadStore]; + [self reloadStore]; return self; } @@ -291,7 +291,7 @@ - (void)clearStore { }); } -- (void)loadStore { +- (void)reloadStore { [self log:@"(Re)loading store..."]; if ([self.delegate respondsToSelector:@selector(ubiquityStoreManager:willLoadStoreIsCloud:)]) @@ -807,7 +807,7 @@ - (void)deleteCloudContainerLocalOnly:(BOOL)localOnly { } if (self.cloudEnabled) - [self loadStore]; + [self reloadStore]; }]; } @@ -828,7 +828,7 @@ - (void)deleteCloudStoreLocalOnly:(BOOL)localOnly { [self createTentativeStoreUUID]; if (self.cloudEnabled) - [self loadStore]; + [self reloadStore]; }]; } @@ -844,7 +844,7 @@ - (void)deleteLocalStore { [self removeItemAtURL:[self URLForLocalStore] localOnly:YES]; if (!self.cloudEnabled) - [self loadStore]; + [self reloadStore]; }]; } @@ -921,7 +921,7 @@ - (void)setCloudEnabled:(BOOL)enabled { [local setBool:enabled forKey:CloudEnabledKey]; [NSFileCoordinator addFilePresenter:self]; - [self loadStore]; + [self reloadStore]; }]; } @@ -1022,7 +1022,7 @@ - (void)applicationDidBecomeActive:(NSNotification *)note { - (void)applicationWillEnterForeground:(NSNotification *)note { - [self loadStore]; + [self reloadStore]; } - (void)applicationDidEnterBackground:(NSNotification *)note { @@ -1065,7 +1065,7 @@ - (void)keyValueStoreChanged:(NSNotification *)note { if (![self checkCloudContentCorruption:nil]) if (self.cloudEnabled && ![_persistentStoreCoordinator.persistentStores count]) // Corruption was removed and our cloud store is not yet loaded. Try loading the store again. - [self loadStore]; + [self reloadStore]; } /** @@ -1124,7 +1124,7 @@ - (void)cloudStoreChanged:(NSNotification *)note { // Reload the store. [self log:@"Cloud store changed. StoreUUID: %@ (%@), Identity: %@", self.storeUUID, _tentativeStoreUUID ? @"tentative" : @"definite", self.currentIdentityToken]; - [self loadStore]; + [self reloadStore]; } - (void)mergeChanges:(NSNotification *)note { @@ -1152,7 +1152,7 @@ - (void)mergeChanges:(NSNotification *)note { // Try to reload the store to see if it's still viable. // If not, either the application will handle it or we'll fall back to the local store. // TODO: Verify that this works reliably. - [self loadStore]; + [self reloadStore]; return; } diff --git a/iCloudStoreManagerExample.xcodeproj/project.pbxproj b/iCloudStoreManagerExample.xcodeproj/project.pbxproj index 331d7ef..7b11615 100644 --- a/iCloudStoreManagerExample.xcodeproj/project.pbxproj +++ b/iCloudStoreManagerExample.xcodeproj/project.pbxproj @@ -29,7 +29,7 @@ 5B4B126F15227DFE00153613 /* User.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B126E15227DFE00153613 /* User.m */; }; 5B4B127215227DFE00153613 /* Event.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4B127115227DFE00153613 /* Event.m */; }; 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3982680A3617FB669BBC5 /* JRSwizzle.m */; }; - 93D39247AA48A93AE9BE359A /* NSManagedObject+UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */; }; + 93D39247AA48A93AE9BE359A /* NSError+UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */; }; DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA3B1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld */; }; DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */; }; /* End PBXBuildFile section */ @@ -75,8 +75,8 @@ 5B5E838C152BAB96009C1991 /* iCloudStoreManagerExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = iCloudStoreManagerExample.entitlements; sourceTree = ""; }; 93D3982680A3617FB669BBC5 /* JRSwizzle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JRSwizzle.m; sourceTree = ""; }; 93D39827ACFB3A3A3E1CEF4C /* JRSwizzle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JRSwizzle.h; sourceTree = ""; }; - 93D39B7366D5AD8A2FEFD7F1 /* NSManagedObject+UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+UbiquityStoreManager.h"; sourceTree = ""; }; - 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+UbiquityStoreManager.m"; sourceTree = ""; }; + 93D39B7366D5AD8A2FEFD7F1 /* NSError+UbiquityStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+UbiquityStoreManager.h"; sourceTree = ""; }; + 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+UbiquityStoreManager.m"; sourceTree = ""; }; DA79AA3115585EBE00BAA07A /* iCloudStoreManagerExample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iCloudStoreManagerExample-Info.plist"; sourceTree = ""; }; DA79AA3215585EBE00BAA07A /* iCloudStoreManagerExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iCloudStoreManagerExample-Prefix.pch"; sourceTree = ""; }; DA79AA3C1558608500BAA07A /* iCloudStoreManager.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = iCloudStoreManager.xcdatamodel; sourceTree = ""; }; @@ -211,8 +211,8 @@ children = ( DA79AA3F1558613F00BAA07A /* UbiquityStoreManager.h */, DA79AA401558613F00BAA07A /* UbiquityStoreManager.m */, - 93D39F19900BD420A6E9B72E /* NSManagedObject+UbiquityStoreManager.m */, - 93D39B7366D5AD8A2FEFD7F1 /* NSManagedObject+UbiquityStoreManager.h */, + 93D39F19900BD420A6E9B72E /* NSError+UbiquityStoreManager.m */, + 93D39B7366D5AD8A2FEFD7F1 /* NSError+UbiquityStoreManager.h */, ); path = iCloudStoreManager; sourceTree = ""; @@ -335,7 +335,7 @@ 5B4B127215227DFE00153613 /* Event.m in Sources */, DA79AA3D1558608500BAA07A /* iCloudStoreManagerExample.xcdatamodeld in Sources */, DA79AA411558613F00BAA07A /* UbiquityStoreManager.m in Sources */, - 93D39247AA48A93AE9BE359A /* NSManagedObject+UbiquityStoreManager.m in Sources */, + 93D39247AA48A93AE9BE359A /* NSError+UbiquityStoreManager.m in Sources */, 93D3906BB1F143AA15A9BDB9 /* JRSwizzle.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iCloudStoreManagerExample/AppDelegate.m b/iCloudStoreManagerExample/AppDelegate.m index d23070e..e009ad0 100644 --- a/iCloudStoreManagerExample/AppDelegate.m +++ b/iCloudStoreManagerExample/AppDelegate.m @@ -6,6 +6,7 @@ // Copyright (c) 2012 Yodel Code LLC. All rights reserved. // +#import #import "AppDelegate.h" #import "MasterViewController.h" #import "DetailViewController.h" @@ -15,6 +16,8 @@ @interface AppDelegate () @property(nonatomic, strong) UIAlertView *handleCloudContentAlert; +@property(nonatomic, strong) UIAlertView *handleCloudContentWarningAlert; + - (NSURL *)storeURL; @end @@ -38,7 +41,23 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( { NSLog(@"Starting iCloudStoreManagerExample on device: %@\n\n", [UIDevice currentDevice].name); - // STEP 1 - Initialize the UbiquityStoreManager + self.handleCloudContentAlert = [[UIAlertView alloc] initWithTitle:@"iCloud Sync Problem" message: + @"\n\n\n\nWaiting for another device to auto‑correct the problem..." + delegate:self + cancelButtonTitle:nil + otherButtonTitles:@"Fix Now", nil]; + UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + activityIndicator.center = CGPointMake(142, 90); + [activityIndicator startAnimating]; + [self.handleCloudContentAlert addSubview:activityIndicator]; + self.handleCloudContentWarningAlert = [[UIAlertView alloc] initWithTitle:@"Fix iCloud Now" message: + @"This problem can usually be auto‑corrected by opening the app on another device where you recently made changes.\n" + @"If you wish to correct the problem from this device anyway, it is possible that recent changes on another device will be lost." + delegate:self + cancelButtonTitle:@"Back" + otherButtonTitles:@"Fix Anyway", nil]; + + // STEP 1 - Initialize the UbiquityStoreManager ubiquityStoreManager = [[UbiquityStoreManager alloc] initStoreNamed:nil withManagedObjectModel:[self managedObjectModel] localStoreURL:[self storeURL] containerIdentifier:nil additionalStoreOptions:nil delegate:self]; @@ -156,18 +175,17 @@ - (User *)primaryUser { - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { - if (alertView == self.handleCloudContentAlert) { - if (buttonIndex == [alertView firstOtherButtonIndex]) - // Disable iCloud - self.ubiquityStoreManager.cloudEnabled = NO; - else if (buttonIndex == [alertView firstOtherButtonIndex] + 1) - // Lose iCloud data - [self.ubiquityStoreManager deleteCloudStoreLocalOnly:NO]; - else if (buttonIndex == [alertView firstOtherButtonIndex] + 2) - // Make iCloud local - [self.ubiquityStoreManager migrateCloudToLocalAndDeleteCloudStoreLocalOnly:NO]; - else if (buttonIndex == [alertView firstOtherButtonIndex] + 3) - // Fix iCloud + if (alertView == self.handleCloudContentAlert && buttonIndex == [alertView firstOtherButtonIndex]) + // Fix Now + [self.handleCloudContentWarningAlert show]; + + if (alertView == self.handleCloudContentWarningAlert) { + if (buttonIndex == alertView.cancelButtonIndex) + // Back + [self.handleCloudContentAlert show]; + + if (buttonIndex == alertView.firstOtherButtonIndex) + // Fix Anyway [self.ubiquityStoreManager rebuildCloudContentFromCloudStore]; } } @@ -204,9 +222,10 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoor __managedObjectContext = moc; __managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; dispatch_async(dispatch_get_main_queue(), ^{ - if ([self.handleCloudContentAlert isVisible]) - [self.handleCloudContentAlert dismissWithClickedButtonIndex:[self.handleCloudContentAlert cancelButtonIndex] - animated:YES]; + [self.handleCloudContentAlert dismissWithClickedButtonIndex:[self.handleCloudContentAlert firstOtherButtonIndex] + 9 + animated:YES]; + [self.handleCloudContentWarningAlert dismissWithClickedButtonIndex:[self.handleCloudContentWarningAlert firstOtherButtonIndex] + 9 + animated:YES]; [masterViewController.iCloudSwitch setOn:isCloudStore animated:YES]; [masterViewController.storeLoadingActivity stopAnimating]; @@ -215,26 +234,12 @@ - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoor - (void)ubiquityStoreManager:(UbiquityStoreManager *)manager handleCloudContentCorruptionIsCloud:(BOOL)isCloudStore { - if ([self.handleCloudContentAlert isVisible]) + if ([self.handleCloudContentAlert isVisible] || [self.handleCloudContentWarningAlert isVisible]) NSLog(@"already showing."); - - dispatch_async(dispatch_get_main_queue(), ^{ - self.handleCloudContentAlert = [[UIAlertView alloc] initWithTitle:@"Problem With iCloud Sync!" message: - @"An error has occurred within Apple's iCloud causing your devices to no longer " - @"sync up properly.\n" - @"To fix this, you can either:\n" - @"- Disable iCloud\n" - @"- Lose your iCloud data and start anew using your local data\n" - @"- Make iCloud local and disable iCloud sync\n" - @"- Fix iCloud sync\n\n" - @"If you 'Make iCloud local', iCloud data will overwrite any local data you may have.\n\n" - @"If you 'Fix iCloud' (same as 'Make iCloud local' and turning iCloud sync on again later), " - @"be mindful on what device you do this on: Any changes on other devices that failed to sync " - @"will be lost." - delegate:self cancelButtonTitle:nil - otherButtonTitles:@"Disable iCloud", @"Lose iCloud data", @"Make iCloud local", @"Fix iCloud", nil]; - [self.handleCloudContentAlert show]; - }); + else + dispatch_async(dispatch_get_main_queue(), ^{ + [self.handleCloudContentAlert show]; + }); } @end From 8aa49aa7bd9ebcc85298ee3483df590bc1d915ed Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Thu, 21 Mar 2013 22:50:36 -0400 Subject: [PATCH 30/35] Add AppCode workspace and code style. --- .gitignore | 10 +-- .idea/.name | 1 + .idea/codeStyleSettings.xml | 90 ++++++++++++++++++++ .idea/encodings.xml | 5 ++ .idea/find.xml | 5 ++ .idea/iCloudStoreManager.iml | 11 +++ .idea/inspectionProfiles/Project_Default.xml | 2 + .idea/misc.xml | 8 ++ .idea/modules.xml | 9 ++ .idea/scopes/scope_settings.xml | 5 ++ .idea/vcs.xml | 8 ++ .idea/xcode.xml | 5 ++ 12 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 .idea/.name create mode 100644 .idea/codeStyleSettings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/find.xml create mode 100644 .idea/iCloudStoreManager.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/xcode.xml diff --git a/.gitignore b/.gitignore index 681dd3c..56e4db0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,8 @@ .DS_Store # IntelliJ -/.idea/* -!/.idea/encodings.xml -!/.idea/inspectionProfiles -!/.idea/projectCodeStyle.xml -!/.idea/validation.xml -*.iml -/*.ipr -/*.iws +.idea/workspace.xml +.idea/dictionaries/ # Xcode IDE /*.xcodeproj/* diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..ffba677 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +iCloudStoreManagerExample \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..f0ca56e --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,90 @@ + + + + + + + diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/find.xml b/.idea/find.xml new file mode 100644 index 0000000..6892330 --- /dev/null +++ b/.idea/find.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/iCloudStoreManager.iml b/.idea/iCloudStoreManager.iml new file mode 100644 index 0000000..f9b36de --- /dev/null +++ b/.idea/iCloudStoreManager.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 9a56338..ddf5da5 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,6 +2,8 @@ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..806e25c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f190de7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c9de466 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/xcode.xml b/.idea/xcode.xml new file mode 100644 index 0000000..685e8b2 --- /dev/null +++ b/.idea/xcode.xml @@ -0,0 +1,5 @@ + + + + + From 0be42fa3c7a424dc873ab530e0e544069c9b4769 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Fri, 22 Mar 2013 00:29:01 -0400 Subject: [PATCH 31/35] Give it all a nice and consistent code format. Thanks AppCode. --- .idea/codeStyleSettings.xml | 2 +- .../NSError+UbiquityStoreManager.h | 3 +- .../NSError+UbiquityStoreManager.m | 5 +- iCloudStoreManager/UbiquityStoreManager.h | 13 +- iCloudStoreManager/UbiquityStoreManager.m | 337 ++++++++++-------- 5 files changed, 193 insertions(+), 167 deletions(-) diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml index f0ca56e..7ae4c38 100644 --- a/.idea/codeStyleSettings.xml +++ b/.idea/codeStyleSettings.xml @@ -68,6 +68,7 @@