Skip to main content

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 key#

If 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.

  1. Add the + (nonnull NSString *)primaryKey to your classes.
  2. Update tracking data:

    // After launching your app, maybe in didFinishLaunchingWithOptions    [self.synchronizer.changeManager updateTrackingForObjectsWithPrimaryKey];
  1. Now you can use SyncKit.

Adding primary key field in your model#

If 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.

  1. Update your CoreData model and add the required field to those model objects that might need one.
  2. Create a Core Data mapping model and add entity mappings for you entities.
  3. In your mapping model, specify QSEntityIdentifierUpdateMigrationPolicy as the Custom Policy. This policy will assign a primary key value to each object being migrated.
  4. 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;}
  1. 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;}
  1. You can now create your QSCloudKitSynchronizer and use it to sync.

Discarding current SyncKit tracking data#

A 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.

  1. Discard all SyncKit data:
[self.synchronizer eraseLocal];

or, alternatively, delete everything in [QSCloudKitSynchronizer storePath]

  1. Delete your store file.
  2. Create a new Core Data stack.
  3. Create a new QSCloudKitSynchronizer.
  4. 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.