Has anyone got iCloud core data syncing setup on Xcode 6 and iOS 8? (hopefully this isn't a duplicate post)
Where did iCloud Core Data storage option go?
I remember Core Data had an extra storage option called Core Data storage, but now in Xcode 6 it only seems to show key-value and document storage when I enable the iCloud toggle in Xcode 6.
Background Info
- New iPad app
- Xcode 6
- Targeting minimum version iOS 7 but hoping it works for iOS 8 too? (We can set iOS 8 as minimum)
- Want to use iCloud Core Data storage instead of key-value or document storage.
- Have logged into the same Apple account in the Settings > iCloud for both Simulator and iPad device
- My provisioning profile used to code sign the app has iCloud enabled for both development and distribution (was automatically enabled by Xcode)
My Setup
So far, I don't know if I've setup Core Data iCloud correctly.
Xcode appears to have setup the iCloud containers in the iOS Developer Portal:
iCloud.com.xxxxxx.xxxxxxxx (note: I've replaced the actual strings with xxxx here) My Xcode 6 iCloud "services" list shows no ticks next to:
- Key-value storage
- iCloud documents
- CloudKit
Which one should we be using now since it doesn't list "core data" as a storage option?
In the "Containers" directly below the "services", it shows the following options greyed out:
- Use default container (this one ticked by default)
- Specify custom containers
- iCloud.com.xxxxxxxxxx.xxxxxxxxx (again, substituted the real identifiers with xxxx)
I can't choose any option, it seems to force me to "Use default container".
Finally, Xcode seems to show ticks for:
- Add the "iCloud" entitlement to your App ID
- Add the "iCloud containers" entitlement to your App ID
- Add the "iCloud" entitlemen to your entitlements file
- Link CloudKit.framework
So by Xcode's own automated process, it setup everything for me.
The Reference Code
OK, so I read around and notice a iCloud stack written here:
https://github.com/mluisbrown/iCloudCoreDataStack
I've taken the necessary code and tried to adapt to my Core Data manager singleton:
DataManager.h file
+ (id)sharedModel; + (ALAssetsLibrary *)sharedLibrary; @property (nonatomic, readonly) NSManagedObjectContext *mainContext; @property (nonatomic, readonly) NSPersistentStoreCoordinator *storeCoordinator; - (NSString *)modelName; - (NSString *)pathToModel; - (NSString *)storeFilename; - (NSString *)pathToLocalStore; #pragma mark - Entity Fetching Methods - -(NSArray *)fetchEntityOfType:(NSString *)entityType UsingPredicated:(NSPredicate *)predicate sortBy:(NSString *)sortKey ascendingOrder:(BOOL)ascendingOrder; DataManager.m file
@property (nonatomic, strong) NSManagedObjectModel *managedObjectModel; - (NSString *)documentsDirectory; @end @implementation MLSAlbumsDataModel @synthesize managedObjectModel = _managedObjectModel; @synthesize storeCoordinator = _storeCoordinator; @synthesize mainContext = _mainContext; + (id)sharedModel { static MLSAlbumsDataModel *__instance = nil; if (__instance == nil) { __instance = [[MLSAlbumsDataModel alloc] init]; } return __instance; } + (ALAssetsLibrary *)sharedLibrary { static ALAssetsLibrary *__instance = nil; if (__instance == nil) { __instance = [[ALAssetsLibrary alloc] init]; } return __instance; } - (NSString *)modelName { return @"Albums"; } - (NSString *)pathToModel { return [[NSBundle mainBundle] pathForResource:[self modelName] ofType:@"momd"]; } - (NSString *)storeFilename { return [[self modelName] stringByAppendingPathExtension:@"sqlite"]; } - (NSString *)pathToLocalStore { return [[self documentsDirectory] stringByAppendingPathComponent:[self storeFilename]]; } - (NSString *)documentsDirectory { NSString *documentsDirectory = nil; NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); documentsDirectory = [paths objectAtIndex:0]; return documentsDirectory; } - (NSManagedObjectContext *)mainContext { if(_mainContext == nil) { _mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; _mainContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy; // setup persistent store coordinator DLog(@"SQLITE STORE PATH: %@", [self pathToLocalStore]); NSURL *storeURL = [NSURL fileURLWithPath:[self pathToLocalStore]]; //_mainContext.persistentStoreCoordinator = [self storeCoordinator]; _mainContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel]; __weak NSPersistentStoreCoordinator *psc = self.mainContext.persistentStoreCoordinator; // iCloud notification subscriptions NSNotificationCenter *dc = [NSNotificationCenter defaultCenter]; [dc addObserver:self selector:@selector(storesWillChange:) name:NSPersistentStoreCoordinatorStoresWillChangeNotification object:psc]; [dc addObserver:self selector:@selector(storesDidChange:) name:NSPersistentStoreCoordinatorStoresDidChangeNotification object:psc]; [dc addObserver:self selector:@selector(persistentStoreDidImportUbiquitousContentChanges:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:psc]; NSError* error; // the only difference in this call that makes the store an iCloud enabled store // is the NSPersistentStoreUbiquitousContentNameKey in options. I use "iCloudStore" // but you can use what you like. For a non-iCloud enabled store, I pass "nil" for options. // Note that the store URL is the same regardless of whether you're using iCloud or not. // If you create a non-iCloud enabled store, it will be created in the App's Documents directory. // An iCloud enabled store will be created below a directory called CoreDataUbiquitySupport // in your App's Documents directory [self.mainContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:@{ NSPersistentStoreUbiquitousContentNameKey : @"iCloudStore" } error:&error]; if (error) { NSLog(@"error: %@", error); } _storeCoordinator = self.mainContext.persistentStoreCoordinator; } return _mainContext; } - (NSManagedObjectModel *)managedObjectModel { if(_managedObjectModel == nil) { NSURL *storeURL = [NSURL fileURLWithPath:[self pathToModel]]; _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:storeURL]; } return _managedObjectModel; } - (NSPersistentStoreCoordinator *)storeCoordinator { if (_storeCoordinator == nil) { // ----------------------------------------------------------------------------------------------------------------------------- // Code moved to managed object context code above // ----------------------------------------------------------------------------------------------------------------------------- /* DLog(@"SQLITE STORE PATH: %@", [self pathToLocalStore]); NSURL *storeURL = [NSURL fileURLWithPath:[self pathToLocalStore]]; NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; NSError *error = nil; if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSDictionary *userInfo = [NSDictionary dictionaryWithObject:error forKey:NSUnderlyingErrorKey]; NSString *reason = @"Could not create persistent store"; NSException *exc = [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:userInfo]; @throw exc; } _storeCoordinator = psc; */ } return _storeCoordinator; } #pragma mark - iCloud Related Methods - // Subscribe to NSPersistentStoreDidImportUbiquitousContentChangesNotification - (void)persistentStoreDidImportUbiquitousContentChanges:(NSNotification*)note { NSLog(@"%s", __PRETTY_FUNCTION__); NSLog(@"%@", note.userInfo.description); NSManagedObjectContext *moc = self.mainContext; [moc performBlock:^{ [moc mergeChangesFromContextDidSaveNotification:note]; DLog(@"NSPersistentStoreDidImportUbiquitousContentChangesNotification executed"); /* // you may want to post a notification here so that which ever part of your app // needs to can react appropriately to what was merged. // An exmaple of how to iterate over what was merged follows, although I wouldn't // recommend doing it here. Better handle it in a delegate or use notifications. // Note that the notification contains NSManagedObjectIDs // and not NSManagedObjects. NSDictionary *changes = note.userInfo; NSMutableSet *allChanges = [NSMutableSet new]; [allChanges unionSet:changes[NSInsertedObjectsKey]]; [allChanges unionSet:changes[NSUpdatedObjectsKey]]; [allChanges unionSet:changes[NSDeletedObjectsKey]]; for (NSManagedObjectID *objID in allChanges) { // do whatever you need to with the NSManagedObjectID // you can retrieve the object from with [moc objectWithID:objID] } */ }]; } // Subscribe to NSPersistentStoreCoordinatorStoresWillChangeNotification // most likely to be called if the user enables / disables iCloud // (either globally, or just for your app) or if the user changes // iCloud accounts. - (void)storesWillChange:(NSNotification *)note { NSManagedObjectContext *moc = self.mainContext; [moc performBlockAndWait:^{ NSError *error = nil; if ([moc hasChanges]) { [moc save:&error]; } [moc reset]; }]; // now reset your UI to be prepared for a totally different // set of data (eg, popToRootViewControllerAnimated:) // but don't load any new data yet. [[NSNotificationCenter defaultCenter] postNotificationName:@"notifCoreDataStoreWillChange" object:nil]; DLog(@"storeWillChange notification fire"); } // Subscribe to NSPersistentStoreCoordinatorStoresDidChangeNotification - (void)storesDidChange:(NSNotification *)note { // here is when you can refresh your UI and // load new data from the new store [[NSNotificationCenter defaultCenter] postNotificationName:@"notifCoreDataStoreDidChange" object:nil]; DLog(@"storeDidChange notification fire"); } #pragma mark - Entity Fetching Methods - -(NSArray *)fetchEntityOfType:(NSString *)entityType UsingPredicated:(NSPredicate *)predicate sortBy:(NSString *)sortKey ascendingOrder:(BOOL)ascendingOrder { NSEntityDescription *entityDescription = [NSEntityDescription entityForName:entityType inManagedObjectContext:[[MLSAlbumsDataModel sharedModel] mainContext]]; NSSortDescriptor *sortDescriptor = nil; if(sortKey) { sortDescriptor = [[NSSortDescriptor alloc] initWithKey:sortKey ascending:ascendingOrder]; } else { sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"updatedAt" ascending:ascendingOrder]; } NSFetchRequest *request = [[NSFetchRequest alloc] init]; request.entity = entityDescription; if(predicate) { request.predicate = predicate; } request.sortDescriptors = @[sortDescriptor]; NSError *error = nil; NSArray *results = [[[MLSAlbumsDataModel sharedModel] mainContext] executeFetchRequest:request error:&error]; if(results == nil) { DLog(@"Error getting entity of type '%@' using predicate '%@', sortKey '%@' ascendingOrder %d", entityType, predicate, sortKey, ascendingOrder); } return results; } My Observations
I tried to run the app on the iPad Simulator (I believe it is the iOS 8 simulator) and on iPad device running iOS 7.x
I created an album with a user entered name on the simulator, but I am not seeing the iPad device showing the newly created album. I also tried reversing the roles, iPad device create, iOS simulator no results either.
I do see my log messages:
storeDidChange notification fire SQLITE STORE PATH: /Users/xxxxxxx/Library/Developer/CoreSimulator/Devices/3DC17576-92E9-4EAF-B77A-41340AE28F92/data/Containers/Data/Application/E51085CE-3772-4DF1-A503-1C243497091A/Documents/Albums.sqlite If I minimise the app in the simulator and open it again (without pressing the Stop button in Xcode), I see these message:
-[PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:](808): CoreData: Ubiquity: nobody~sim301AE3E8-16B2-5A08-917D-7B55D1879BE4:iCloudStore Using local storage: 1 I read that "Using local storage: 0" is what it should ideally be? and that 1 means local device data store rather than iCloud data store.
When I create an album, save it, stop the simulator, then start the app again, my albums disappears, but immediately after I create a new album, all the previous album reappear magically again. It's a bit odd. If I don't use iCloud and revert my code to previous setup, I can create and see my album fine, regardless of whether I minimise my app or not, or restart app, but then I don't have iCloud sync which I need.
Have I made any mistakes anywhere?
Sorry for the long post but has anyone got iCloud working for iOS 8 and Xcode 6 ?
I could really use some help.
Extra Questions
1) Does iOS 8 require the use of this container identifier ? (which Xcode 6 generated for me):
com.apple.developer.icloud-container-identifiers That's not what the iOS 7 one looks like right? iOS 7 one is more like:
com.apple.developer.ubiquity-container-identifiers 2) Do I need an iCloud Drive account before it works?
Super confused @_@