Core Data CloudKit Xcoders Talk - Amazon Simple Storage Service (S3)

1 downloads 91 Views 6MB Size Report
Used by Apple. iCloud Drive & iCloud ... to alert you of changes. Uses the remote notification system in iOS .... to
Integrating Core Data and CloudKit Jared Sorge

Scorebook Remember Your Games

Core Data Paul Goracke – “Core Data Potpurri”, February 2014 http://bit.ly/1A5fWGr Marcus Zarra – “My Core Data Stack”, March 2015 http://bit.ly/1KQaibt TaphouseKit – GitHub Project http://bit.ly/1e4AEwo

CloudKit OS X Yosemite & iOS 8 Transport layer No black magic

CloudKit Used by Apple iCloud Drive & iCloud Photo Library Used by third parties 1Password

CloudKit Stack

CKContainer

CloudKit Stack

Public CKDatabase

Private CKDatabase

CKContainer

CloudKit Stack CKRecordZone Default Zone Public CKDatabase

Custom

Private CKDatabase

CKContainer

CloudKit Stack CKRecord CKRecordZone Default Zone Public CKDatabase

Custom

Private CKDatabase

CKContainer

CloudKit Stack CKSubscription (optional) CKRecord CKRecordZone Default Zone Public CKDatabase

Custom

Private CKDatabase

CKContainer

CloudKit Stack CKSubscription (optional) CKRecord CKRecordZone Default Zone Public CKDatabase

Custom

Private CKDatabase

CKContainer

CKRecord Store data using key/value pairs NSString, NSNumber, NSData, NSDate, NSArray, CLLocation, CKAsset, CKReference Use constant strings for keys recordType property is like a database table name

CKRecord Initializers initWithRecordType: initWithRecordType:zoneID: initWithRecordType:recordID:

CKRecordID 2 properties recordName, zoneID Initializers initWithRecordName: initWithRecordName:zoneID:

CKRecordZoneID initWithZoneName:ownerName: Use CKOwnerDefaultName for ownerName Zone name is a string Use CKRecordZoneDefaultName for the default zone

CKRecordZoneID CKContainer *container = [CKContainer sharedContainer]; CKDatabase *privateDB = [container privateCloudDatabase]; CKRecordZoneID *newZone = [[CKRecordZoneID alloc] initWithZoneName:@"ScorebookData" ownerName:CKOwnerDefaultName]; [privateDB saveRecordZone:newZone completionHandler:^(CDRecordZone *zone, NSError *error) { //Error handling //Additional configuration }];

CKRecord Creation CKRecordZoneID *zone = // CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:self.ckRecordName zoneID:zone]; CKRecord *gameRecord = [[CKRecord alloc] initWithRecordType:[SBGame entityName] recordID:recordID]; [gameRecord setObject:self.title forKey SBGameTitleKEY]; gameRecord[SBGameScoringTypeKEY] = @(self.scoringType); gameRecord[SBGamePointsToWinKEY] = @(self.pointsToWin);

CKAsset Blob storage Single initializer initWithFileURL: No support for NSData Attach to CKRecord instance as a value

CKAsset NSURL *fileURL = // … url of path to file CKAsset *asset = [[CKAsset alloc] initWithFileURL:fileURL]; CKRecord *record = // record[@“asset”] = asset;

CKReference Relate records with separate record types Relationships in a single zone only 1:many relationships many:many not officially supported Associate on the many side of the relationship Person

Player

CKReference Initializers -initWithRecordID:action: -initWithRecord:action:

Set the delete action on the initializer CKReferenceActionDeleteSelf CKReferenceActionNone

CKReference CKRecordID *personRecordID = // CKReference *personReference = [[CKReference alloc] initWithRecordID:personRecordID action:CKReferenceActionDeleteSelf]; CKRecord *playerRecord = // playerRecord[SBPlayerPersonKEY] = personReference;

CKSubscription Subscribes to changes of a record type or custom zone Can use a search predicate to determine matches Uses push notifications to alert you of changes Uses the remote notification system in iOS

CKSubscription Save to the database for activation on a device Save once, then retrieve on other devices

CKSubscription CKRecordZoneID *userRecordZone = // [privateDB fetchAllSubscriptionsWithCompletionHandler:^(NSArray *subs, NSError *error) { CKSubscription *subscription = [subs firstObject]; if (subscription == nil) { subscription =[[CKSubscription alloc] initWithZoneID:userRecordZone options:0]; }

}];

[privateDB saveSubscription:scorebookDataSubscription completionHandler:^(CKSubscription *sub, NSError *error) { //handle error }];

CloudKit Stack CKSubscription (optional) CKRecord CKRecordZone Default Zone Public CKDatabase

Custom

Private CKDatabase

CKContainer

Putting it together Sync is about upload, download, and conflict handling

Make a Plan Database: public, private, or both? Using a custom record zone? Are you happy with your Core Data object graph? How to make specific things generic, and generic things specific?

SBCloudKitCompatible @protocol SBCloudKitCompatible //Add to entities @property (nonatomic, strong) NSString *ckRecordName; @property (nonatomic, strong) NSDate *modificationDate; //Add to categories on the model objects - (CKRecord *)cloudKitRecordInRecordZone:(CKRecordZoneID *)zone; + (NSManagedObject *)managedObjectFromRecord:(CKRecord *)record context:(NSManagedObjectContext *)context; @end

ckRecordName - (void)awakeFromInsert { [super awakeFromInsert]; NSString *uuid = [[NSUUID UUID] UUIDString]; NSString *recordName = [NSString stringWithFormat: @“SBGame|~|%@“, uuid]; /* SBGame|~|386c1919-5f25-4be2-975f-5b34506c51db */ }

self.ckRecordName = recordName;

modificationDate - (void)processCoreDataWillSaveNotification:(NSNotification *)notification { NSManagedObjectContext *context = // monitored context NSSet *inserted = [context insertedObjects]; NSSet *updated = [context updatedObjects]; if (inserted.count == 0 && updated.count == 0) { return; } for (id managedObject in inserted) { managedObject.modificationDate = [NSDate date]; } for (id managedObject in updated) { managedObject.modificationDate = [NSDate date]; } }

modificationDate - (void)processCoreDataWillSaveNotification:(NSNotification *)notification { NSManagedObjectContext *context = // monitored context NSSet *inserted = [context insertedObjects]; NSSet *updated = [context updatedObjects]; if (inserted.count == 0 && updated.count == 0) { return; } for (id managedObject in inserted) { managedObject.modificationDate = [NSDate date]; } for (id managedObject in updated) { managedObject.modificationDate = [NSDate date]; } }

cloudKitRecordInRecordZone: - (CKRecord *)cloudKitRecordInRecordZone:(CKRecordZoneID *)zone { CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:self.ckRecordName zoneID:zone]; NSString *entityName = [SBPerson entityName]; CKRecord *personRecord = [[CKRecord alloc] initWithRecordType:entityName recordID:recordID]; personRecord[SBPersonFirstNameKEY] = self.firstName; personRecord[SBPersonLastNameKEY] = self.lastName; personRecord[SBPersonEmailAddressKEY] = self.emailAddress; if (self.imageURL) { CKAsset *imageAsset = [[CKAsset alloc] initWithFileURL:[self urlForImage]]; personRecord[SBPersonAvatarKEY] = imageAsset; } return personRecord; }

managedObjectFromRecord:context: + (instancetype)managedObjectFromRecord:(CKRecord *)ckRecord context:(NSManagedObjectContext *)context { CKRecordID *recordID = ckRecord.recordID; SBMatch *match = [SBMatch matchWithCloudKitRecordName:recordID.recordName managedObjectContext:context]; if (match.modificationDate != nil && ckRecord.modificationDate < match.modificationDate) { return match; } match.date = ckRecord[SBMatchDateKEY]; match.monthYear = ckRecord[SBMatchMonthYearKEY]; match.finished = [ckRecord[SBMatchFinishedKEY] boolValue]; match.note = [ckRecord objectForKey:SBMatchNoteKEY]; …

managedObjectFromRecord:context: … CKReference *gameRef = ckRecord[SBMatchGameKEY]; if (gameRef != nil) { SBGame *game = [SBGame gameWithCloudKitRecordID:gameRef.recordID managedObjectContext:context]; match.game = game; } return match; }

Upload to CloudKit Monitor NSManagedObjectContextDidSaveNotification on the main thread NSManagedObjectContext Gather the objects from the userInfo dictionary in the posted notification Convert the inserted/updated objects into an array of CKRecords

Convert to CKRecord CKRecordZoneID *zone = // NSMutableArray *savedRecords = [NSMutableArray array]; for (id object in insertedObjects) { CKRecord *record = [object cloudKitRecordInRecordZone:zone]; [savedRecords addObject:record]; }

Upload to CloudKit Monitor NSManagedObjectContextDidSaveNotification on the main thread NSManagedObjectContext Gather the objects from the userInfo dictionary in the posted notification Convert the inserted/updated objects into an array of CKRecords Convert the deleted objects into an array of CKRecordIDs

Convert to CKRecordID CKRecordZoneID *zone = // NSMutableArray *deletedRecords = [NSMutableArray array]; for (id managedObject in deletedObjects) { CKRecordID *deletedRecordID = [[CKRecordID alloc] initWithRecordName:managedObject.ckRecordName zoneID:zone]; [deletedRecords addObject:deletedRecordID]; } }

Upload to CloudKit Monitor NSManagedObjectContextDidSaveNotification on the main thread NSManagedObjectContext Gather the objects from the userInfo dictionary in the posted notification Convert the inserted/updated objects into an array of CKRecords Convert the deleted objects into an array of CKRecordIDs Use a CKModifyRecordsOperation to send the arrays to CloudKit

Upload to CloudKit CKDatabase *database = // CKModifyRecordsOperation *modifyRecords = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:savedRecords recordIDsToDelete:deletedRecords]; //Called only on the saved records array modifyRecords.perRecordCompletionBlock = ^(CKRecord *record, NSError *error) { //Handle Error }; modifyRecords.modifyRecordsCompletionBlock = ^(NSArray *saved, NSArray *deleted, NSError *error) { //Handle error, perform cleanup }; modifyRecords.savePolicy = CKRecordSaveAllKeys; [database addOperation:modifyRecords];

Upload to CloudKit – Errors Up to the developer to handle CKRecord & CKRecordID conform to NSSecureCoding Failed records persist to disk

Upload to CloudKit – Errors Before uploading, check to see if there are records on disk If so, convert them back to their original state and add to proper array Remove the files from disk

Download from CloudKit

Download from CloudKit (how I do it)

CKFetchRecordChangesOperation initWithRecordZoneID:previousServerChangeToken: Set some block properties void (^recordChangedBlock)(CKRecord *record)

recordChangedBlock NSMutableArray *objectsToMake = [NSMutableArray array]; fetchChanged.recordChangedBlock = ^(CKRecord *record) { [objectsToMake addObject:record]; };

CKFetchRecordChangesOperation initWithRecordZoneID:previousServerChangeToken: Set some block properties void (^recordChangedBlock)(CKRecord *record) void (^recordWithIDWasDeletedBlock)( CKRecordID *recordID)

recordWithIDWasDeletedBlock NSMutableArray *objectsToDelete = [NSMutableArray array]; fetchChanged.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID) { [objectsToDelete addObject:recordID]; };

CKFetchRecordChangesOperation initWithRecordZoneID:previousServerChangeToken: Set some block properties void (^recordChangedBlock)(CKRecord *record) void (^recordWithIDWasDeletedBlock)( CKRecordID *recordID) void (^fetchRecordChangesCompletionBlock)(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError)

fetchRecordChangesCompletionBlock fetchChanged.fetchRecordChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError) { if (operationError) { //Handle error } if (objectsToMake.count > 0 || objectsToDelete.count > 0) { SBCloudKitDownloader *downloader = [[SBCloudKitDownloader alloc] initWithCloudKitRecordsToMake:objectsToMake recordIDsToDelete:objectsToDelete managedObjectContext:// ]; [downloader processIncoming]; } //Save the new change token };

CKFetchRecordChangesOperation initWithRecordZoneID:previousServerChangeToken: Set some block properties void (^recordChangedBlock)(CKRecord *record) void (^recordWithIDWasDeletedBlock)( CKRecordID *recordID) void (^fetchRecordChangesCompletionBlock)(CKServerChangeToken *serverChangeToken, NSData *clientChangeTokenData, NSError *operationError) Add the operation to the database

CKRecord to NSManagedObject NSManagedObjectContext *context = // NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; backgroundContext.parentContext = context; for (CKRecord *record in self.records) { Class entity = NSClassFromString(record.recordType); if ([entity conformsToProtocol:@protocol(SBCloudKitCompatible) ]) { id cloudKitEntity = (id)entity; [cloudKitEntity managedObjectFromRecord:record context:backgroundContext]; } }

Deleting a CKRecordID for (CKRecordID *recordID in self.recordsToDelete) { NSString *recordName = recordID.recordName; NSString *recordType = [[recordName componentsSeparatedByString:@"|~|"] firstObject]; NSPredicate *predicate = [NSPredicate predicateWithFormat: @"ckRecordName = %@", recordName]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:recordType]; fetchRequest.predicate = predicate; NSError *searchError = nil; NSArray *foundObjects = [backgroundContext executeFetchRequest:fetchRequest error:&searchError]; id foundObject = [foundObjects firstObject]; if (foundObject != nil) { [backgroundContext deleteObject:foundObject]; } }

Summary

Upload Process NSManagedObjectContextDidSaveNotification

Convert NSManagedObjects to CKRecord/CKRecordID

Upload to CloudKit

Handle Errors

Download Process Fetch changes in zone from previous change

Convert CKRecords & IDs using background context

Save the context

Save the change token

WWDC! No significant API changes (unless you count nullable/nonnull annotations) But, there’s a Javascript library!

Thank you! Jared Sorge @jsorge http://jsorge.net