Integrating Core Data and CloudKit Jared Sorge Scorebook Remember - - PowerPoint PPT Presentation

integrating core data and cloudkit
SMART_READER_LITE
LIVE PREVIEW

Integrating Core Data and CloudKit Jared Sorge Scorebook Remember - - PowerPoint PPT Presentation

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


slide-1
SLIDE 1

Integrating Core Data and CloudKit

Jared Sorge

slide-2
SLIDE 2

Scorebook

Remember Your Games

slide-3
SLIDE 3

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

slide-4
SLIDE 4

CloudKit

OS X Yosemite & iOS 8 Transport layer No black magic

slide-5
SLIDE 5

CloudKit

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

slide-6
SLIDE 6

CloudKit Stack

CKContainer

slide-7
SLIDE 7

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase

slide-8
SLIDE 8

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase CKRecordZone Default Zone Custom

slide-9
SLIDE 9

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase CKRecordZone Default Zone Custom CKRecord

slide-10
SLIDE 10

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase CKRecordZone Default Zone CKRecord CKSubscription (optional) Custom

slide-11
SLIDE 11

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase CKRecordZone Default Zone CKRecord CKSubscription (optional) Custom

slide-12
SLIDE 12

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

slide-13
SLIDE 13

CKRecord

Initializers initWithRecordType: initWithRecordType:zoneID: initWithRecordType:recordID:

slide-14
SLIDE 14

CKRecordID

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

slide-15
SLIDE 15

CKRecordZoneID

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

slide-16
SLIDE 16

CKRecordZoneID

CKContainer *container = [CKContainer sharedContainer]; CKDatabase *privateDB = [container privateCloudDatabase]; CKRecordZoneID *newZone = [[CKRecordZoneID alloc] initWithZoneName:@"ScorebookData"

  • wnerName:CKOwnerDefaultName];

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

  • wnerName:CKOwnerDefaultName];
slide-17
SLIDE 17

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); CKRecordZoneID *zone = // CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:self.ckRecordName zoneID:zone]; CKRecord *gameRecord = [[CKRecord alloc] initWithRecordType:[SBGame entityName] recordID:recordID];

slide-18
SLIDE 18

CKAsset

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

slide-19
SLIDE 19

CKAsset

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

slide-20
SLIDE 20

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

slide-21
SLIDE 21

CKReference

Initializers

  • initWithRecordID:action:
  • initWithRecord:action:

Set the delete action on the initializer

CKReferenceActionDeleteSelf CKReferenceActionNone

slide-22
SLIDE 22

CKReference

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

slide-23
SLIDE 23

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

slide-24
SLIDE 24

CKSubscription

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

slide-25
SLIDE 25

CKSubscription

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

  • ptions:0];

} [privateDB saveSubscription:scorebookDataSubscription completionHandler:^(CKSubscription *sub, NSError *error) { //handle error }]; }]; CKSubscription *subscription = [subs firstObject]; if (subscription == nil) { subscription =[[CKSubscription alloc] initWithZoneID:userRecordZone

  • ptions:0];

}

slide-26
SLIDE 26

CloudKit Stack

CKContainer Public CKDatabase Private CKDatabase CKRecordZone Default Zone CKRecord CKSubscription (optional) Custom

slide-27
SLIDE 27

Putting it together

Sync is about upload, download, and conflict handling

slide-28
SLIDE 28

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?

slide-29
SLIDE 29

SBCloudKitCompatible

@protocol SBCloudKitCompatible <NSObject> //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

slide-30
SLIDE 30

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; }

slide-31
SLIDE 31

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<SBCloudKitCompatible> managedObject in inserted) { managedObject.modificationDate = [NSDate date]; } for (id<SBCloudKitCompatible> managedObject in updated) { managedObject.modificationDate = [NSDate date]; } } NSManagedObjectContext *context = // monitored context NSSet *inserted = [context insertedObjects]; NSSet *updated = [context updatedObjects]; if (inserted.count == 0 && updated.count == 0) { return; }

slide-32
SLIDE 32

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<SBCloudKitCompatible> managedObject in inserted) { managedObject.modificationDate = [NSDate date]; } for (id<SBCloudKitCompatible> managedObject in updated) { managedObject.modificationDate = [NSDate date]; } } for (id<SBCloudKitCompatible> managedObject in inserted) { managedObject.modificationDate = [NSDate date]; }

slide-33
SLIDE 33

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; } CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:self.ckRecordName zoneID:zone]; NSString *entityName = [SBPerson entityName]; CKRecord *personRecord = [[CKRecord alloc] initWithRecordType:entityName recordID:recordID];

slide-34
SLIDE 34

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]; … SBMatch *match = [SBMatch matchWithCloudKitRecordName:recordID.recordName managedObjectContext:context];

slide-35
SLIDE 35

managedObjectFromRecord:context:

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

slide-36
SLIDE 36

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

slide-37
SLIDE 37

Convert to CKRecord

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

slide-38
SLIDE 38

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

slide-39
SLIDE 39

Convert to CKRecordID

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

slide-40
SLIDE 40

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

slide-41
SLIDE 41

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]; CKModifyRecordsOperation *modifyRecords = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:savedRecords recordIDsToDelete:deletedRecords];

slide-42
SLIDE 42

Upload to CloudKit – Errors

Up to the developer to handle CKRecord & CKRecordID conform to NSSecureCoding Failed records persist to disk

slide-43
SLIDE 43

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

slide-44
SLIDE 44

Download from CloudKit

slide-45
SLIDE 45

Download from CloudKit (how I do it)

slide-46
SLIDE 46

CKFetchRecordChangesOperation

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

slide-47
SLIDE 47

recordChangedBlock

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

slide-48
SLIDE 48

CKFetchRecordChangesOperation

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

slide-49
SLIDE 49

recordWithIDWasDeletedBlock

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

slide-50
SLIDE 50

CKFetchRecordChangesOperation

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

slide-51
SLIDE 51

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 }; SBCloudKitDownloader *downloader = [[SBCloudKitDownloader alloc] initWithCloudKitRecordsToMake:objectsToMake recordIDsToDelete:objectsToDelete managedObjectContext:// ]; [downloader processIncoming];

slide-52
SLIDE 52

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

slide-53
SLIDE 53

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<SBCloudKitCompatible> cloudKitEntity = (id<SBCloudKitCompatible>)entity; [cloudKitEntity managedObjectFromRecord:record context:backgroundContext]; } } Class entity = NSClassFromString(record.recordType); if ([entity conformsToProtocol:@protocol(SBCloudKitCompatible) ]) { id<SBCloudKitCompatible> cloudKitEntity = (id<SBCloudKitCompatible>)entity; [cloudKitEntity managedObjectFromRecord:record context:backgroundContext]; }

slide-54
SLIDE 54

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]; } } NSString *recordName = recordID.recordName; NSString *recordType = [[recordName componentsSeparatedByString:@"|~|"] firstObject];

slide-55
SLIDE 55

Summary

slide-56
SLIDE 56

Upload Process

NSManagedObjectContextDidSaveNotification Convert NSManagedObjects to CKRecord/CKRecordID Upload to CloudKit Handle Errors

slide-57
SLIDE 57

Download Process

Fetch changes in zone from previous change Convert CKRecords & IDs using background context Save the context Save the change token

slide-58
SLIDE 58

WWDC!

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

slide-59
SLIDE 59

Thank you!

Jared Sorge @jsorge http://jsorge.net