Migrating from SyncKit 0.3.0
If you were using SyncKit before 0.3.0 and you want to adopt the QSPrimaryKey
protocol this page offers some guidance. There are different methods that you could adopt based on your app.
#
Existing primary keyIf your objects already had a populated primary key: Make them implement QSPrimaryKey
and call updateTrackingForObjectsWithPrimaryKey
on the change manager to make it update its object tracking data so it uses the primary key.
- Add the
+ (nonnull NSString *)primaryKey
to your classes. - Update tracking data:
// After launching your app, maybe in didFinishLaunchingWithOptions [self.synchronizer.changeManager updateTrackingForObjectsWithPrimaryKey];
- Now you can use SyncKit.
#
Adding primary key field in your modelIf you need to change your model to add a primary key then this will require a migration, which would break all the tracking data in SyncKit. To avoid that you can use QSEntityIdentifierUpdateMigrationPolicy
as the policy in your mapping model, then call [QSCloudKitSynchronizer updateIdentifierMigrationPolicy];
before starting the migration.
- Update your CoreData model and add the required field to those model objects that might need one.
- Create a Core Data mapping model and add entity mappings for you entities.
- In your mapping model, specify
QSEntityIdentifierUpdateMigrationPolicy
as the Custom Policy. This policy will assign a primary key value to each object being migrated. - When you create your Core Data stack, check if you need to perform a migration. Sample code:
- (MigrationNeededEnum)isMigrationNecessaryForStore:(NSString *)storePath{ if (![[NSFileManager defaultManager] fileExistsAtPath:self.storePath]) { return NO; } NSError *error = nil; NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:[NSURL fileURLWithPath:storePath] options:@{NSInferMappingModelAutomaticallyOption: @YES, NSMigratePersistentStoresAutomaticallyOption: @YES} error:&error];
NSManagedObjectModel *destinationModel = self.coordinator.managedObjectModel; if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata]) { return MigrationOptionNoMigration; } NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:@[[NSBundle mainBundle]] forStoreMetadata:sourceMetadata]; if ([NSMappingModel inferredMappingModelForSourceModel:sourceModel destinationModel:destinationModel error:&error]) { return MigrationOptionLightweightMigration; } return MigrationOptionCustomMigration;}
- Perform migration. Sample code:
- (void)performBackgroundManagedMigrationForStore:(NSString *)sourceStore{ //Show UI // Show some modal screen with some UIActivityIndicatorView or similar // Determine if this migration is being run to adopt primary keys, maybe checking [NSManagedObjectModel versionIdentifiers] if you've been setting those on your model if (useSyncKitMigrationPolity) { [QSCloudKitSynchronizer updateIdentifierMigrationPolicy]; } // Perform migration in the background, so it doesn't freeze the UI. // This way progress can be shown to the user dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ BOOL done = [self migrateStore:sourceStore]; if(done) { dispatch_async(dispatch_get_main_queue(), ^{ if (useSyncKitMigrationPolity) { [QSEntityIdentifierUpdateMigrationPolicy setCoreDataStack:nil]; } // Dismiss UI // Load your persistent store [self loadStore]; }); } });}
- (BOOL)migrateStore:(NSString *)sourceStore{ BOOL success = NO; NSError *error = nil; // STEP 1 - Gather the Source, Destination and Mapping Model NSURL *storeURL = [NSURL fileURLWithPath:sourceStore]; NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeURL options:nil error:&error]; NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:sourceMetadata]; NSManagedObjectModel *destinationModel = self.model; NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil forSourceModel:sourceModel destinationModel:destinationModel]; // STEP 2 - Perform migration, assuming the mapping model isn't null if (mappingModel) { NSError *error = nil; NSMigrationManager *migrationManager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel]; [migrationManager addObserver:self forKeyPath:@"migrationProgress" options:NSKeyValueObservingOptionNew context:NULL]; NSString *destinationStorePath = [[self applicationStoresPath] stringByAppendingPathComponent:@"Temp.sqlite"]; success = [migrationManager migrateStoreFromURL:storeURL type:NSSQLiteStoreType options:nil withMappingModel:mappingModel toDestinationURL:[NSURL fileURLWithPath:destinationStorePath] destinationType:NSSQLiteStoreType destinationOptions:nil error:&error]; [migrationManager removeObserver:self forKeyPath:@"migrationProgress"]; if (success) { // STEP 3 - Replace the old store files with the new migrated store files [self replaceStore:sourceStore withStore:destinationStorePath]; } else { NSLog(@"FAILED MIGRATION: %@",error); } } else { NSLog(@"FAILED MIGRATION: Mapping Model is null"); } return YES; // indicates migration has finished, regardless of outcome}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([object isKindOfClass:[NSMigrationManager class]]) { CGFloat progress = [(NSMigrationManager *)object migrationProgress]; dispatch_async(dispatch_get_main_queue(), ^{ // Update progress indicator UI }); }}
- (BOOL)replaceStore:(NSString *)old withStore:(NSString *)newPath { BOOL success = NO; NSError *error = nil; if ([[NSFileManager defaultManager] fileExistsAtPath:old]) { [[NSFileManager defaultManager] removeItemAtPath:old error:&error]; NSString *walPath = [old stringByAppendingString:@"-wal"]; if ([[NSFileManager defaultManager] fileExistsAtPath:walPath]) { [[NSFileManager defaultManager] removeItemAtPath:walPath error:&error]; } NSString *shmPath = [old stringByAppendingString:@"-shm"]; if ([[NSFileManager defaultManager] fileExistsAtPath:shmPath]) { [[NSFileManager defaultManager] removeItemAtPath:shmPath error:&error]; } error = nil; if ([[NSFileManager defaultManager] moveItemAtPath:newPath toPath:old error:&error]) { NSString *newWalPath = [newPath stringByAppendingString:@"-wal"]; [[NSFileManager defaultManager] moveItemAtPath:newWalPath toPath:walPath error:&error]; NSString *newShmPath = [newPath stringByAppendingString:@"-shm"]; [[NSFileManager defaultManager] moveItemAtPath:newShmPath toPath:shmPath error:&error]; success = YES; } } return success;}
- You can now create your
QSCloudKitSynchronizer
and use it to sync.
#
Discarding current SyncKit tracking dataA less optimal, though easier option would be discarding all your existing data when the app performs a migration to add primary key fields, then synchronizing with an empty NSPersistentStore to restore using data currently on iCloud.
- Discard all SyncKit data:
[self.synchronizer eraseLocal];
or, alternatively, delete everything in [QSCloudKitSynchronizer storePath]
- Delete your store file.
- Create a new Core Data stack.
- Create a new
QSCloudKitSynchronizer
. - Sync. This will download all the data in iCloud to the new, empty store, so at the end of this the local stack will be a copy of the data on iCloud.