Practical CloudKit

A77456b262557e22986345f6d0555c58?s=47 nakajijapan
September 17, 2017

Practical CloudKit

iOSDC 2017 day 2

I talked about sync.

A77456b262557e22986345f6d0555c58?s=128

nakajijapan

September 17, 2017
Tweet

Transcript

  1. J04%$ Practical CloudKit

  2. @nakajijapan Software Engineer Cookpad Inc. iOS / Web / OS

    X
  3. NKJMultiMovieCaptureView NKJMovieComposer NKJPagerViewController PhotoSlider Teiten GitHub Sengiri Shari Ajimi Kazaguruma

    iOSDC 2017
  4. Agenda • Background • What is CloudKit ? • About

    sync • Wrap up
  5. Background

  6. Background • Want the memo application for myself • Want

    to apply color in markdown only • Don’t need the rich editor • Make it available to use everywhere
  7. 2014

  8. None
  9. None
  10. What is CloudKit?

  11. What is CloudKit?

  12. What is CloudKit? • Access to iCloud servers • Supported

    on OS X and iOS and Web • Uses iCloud accounts • Public and private databases • Structured and bulk data • Transport, not local persistence
  13. Default Zone Public Database CloudKit Container Default Zone Default Zone

    Record Default Zone Private Database Default Zone Default Zone Record Custom Zone Default Zone Default Zone Record Shared Zone Shared Database Default Zone Default Zone Record
  14. Default Zone Public Database CloudKit Container Default Zone Default Zone

    Record Default Zone Private Database Default Zone Default Zone Record Custom Zone Default Zone Default Zone Record Shared Zone Shared Database Default Zone Default Zone Record
  15. Why

  16. Why? • Authentication • Use iCloud Account • PushNotification •

    Don’t need to prepare the certificate
  17. Why? • Authentication • Use iCloud Account • PushNotification •

    Don’t need to prepare the certificate • A genuine interest
  18. Core Data iCloud

  19. Core Data iCloud As of macOS v10.12 and iOS 10.0;

    Core Data's iCloud integration feature has been deprecated. Apps will continue to work. There are no changes to or removal of the functionality in macOS 10.12 and iOS 10. Historically, deprecated symbols in Cocoa remain functional for a considerable period of time before removal. Only the client side Core Data iCloud API symbols are deprecated. Core Data with iCloud is built on top of the iCloud Drive service. The service pieces are not effected in any way. If and when the deprecated APIs are disabled in some future OS version, applications running on iOS 9 or 10 will continue to work. IUUQTEFWFMPQFSBQQMFDPNMJCSBSZDPOUFOUSFMFBTFOPUFT(FOFSBM8IBU/FX$PSF%BUB3FMFBTF/PUFTIUNM
  20. Core Data iCloud As of macOS v10.12 and iOS 10.0;

    Core Data's iCloud integration feature has been deprecated. Apps will continue to work. There are no changes to or removal of the functionality in macOS 10.12 and iOS 10. Historically, deprecated symbols in Cocoa remain functional for a considerable period of time before removal. Only the client side Core Data iCloud API symbols are deprecated. Core Data with iCloud is built on top of the iCloud Drive service. The service pieces are not effected in any way. If and when the deprecated APIs are disabled in some future OS version, applications running on iOS 9 or 10 will continue to work. IUUQTEFWFMPQFSBQQMFDPNMJCSBSZDPOUFOUSFMFBTFOPUFT(FOFSBM8IBU/FX$PSF%BUB3FMFBTF/PUFTIUNM
  21. CloudKit

  22. The thing we should think when we make the app

    such that.
  23. None
  24. Sync

  25. Sync

  26. Sync • Subscribe to changes • Fetch server changes •

    Save server changes and token • Send changes to the server • Track local changes • Resolve conflicts
  27. Online

  28. Online

  29. Offline

  30. Offline

  31. Subscribe to changes

  32. Subscribe to changes • On app launch • Prepare for

    fetching server changes • Setup the push notifications for changes
  33. Subscribe to changes privateCloudDatabase.fetchAllSubscriptions { subscriptions, error in guard let

    subscriptions = subscriptions else { return } guard subscriptions.isEmpty else { return } let predicate = NSPredicate(value: true) let querySubscription = CKQuerySubscription( recordType: "Notes", predicate: predicate, options: [ .firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion ]) let notification = CKNotificationInfo() notification.alertBody = "A note was changed or deleted" notification.shouldBadge = true notification.shouldSendContentAvailable = true querySubscription.notificationInfo = notification self.privateCloudDatabase.save(querySubscription) { subscription, error in // Check Error } }
  34. Subscribe to changes privateCloudDatabase.fetchAllSubscriptions { subscriptions, error in guard let

    subscriptions = subscriptions else { return } guard subscriptions.isEmpty else { return } let predicate = NSPredicate(value: true) let querySubscription = CKQuerySubscription( recordType: "Notes", predicate: predicate, options: [ .firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion ]) let notification = CKNotificationInfo() notification.alertBody = "A note was changed or deleted" notification.shouldBadge = true notification.shouldSendContentAvailable = true querySubscription.notificationInfo = notification self.privateCloudDatabase.save(querySubscription) { subscription, error in // Check Error } } Fetch All Sbuscriptions
  35. Subscribe to changes privateCloudDatabase.fetchAllSubscriptions { subscriptions, error in guard let

    subscriptions = subscriptions else { return } guard subscriptions.isEmpty else { return } let predicate = NSPredicate(value: true) let querySubscription = CKQuerySubscription( recordType: "Notes", predicate: predicate, options: [ .firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion ]) let notification = CKNotificationInfo() notification.alertBody = "A note was changed or deleted" notification.shouldBadge = true notification.shouldSendContentAvailable = true querySubscription.notificationInfo = notification self.privateCloudDatabase.save(querySubscription) { subscription, error in // Check Error } } Should control the registration of subscription ourself
  36. Subscribe to changes privateCloudDatabase.fetchAllSubscriptions { subscriptions, error in guard let

    subscriptions = subscriptions else { return } guard subscriptions.isEmpty else { return } let predicate = NSPredicate(value: true) let querySubscription = CKQuerySubscription( recordType: "Notes", predicate: predicate, options: [ .firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion ]) let notification = CKNotificationInfo() notification.alertBody = "A note was changed or deleted" notification.shouldBadge = true notification.shouldSendContentAvailable = true querySubscription.notificationInfo = notification self.privateCloudDatabase.save(querySubscription) { subscription, error in // Check Error } } @available(iOS 10.0, *) public struct CKQuerySubscriptionOptions : OptionSet { public init(rawValue: UInt) public static var firesOnRecordCreation: CKQuerySubscriptionOptions { get } public static var firesOnRecordUpdate: CKQuerySubscriptionOptions { get } public static var firesOnRecordDeletion: CKQuerySubscriptionOptions { get } public static var firesOnce: CKQuerySubscriptionOptions { get } }
  37. Subscribe to changes privateCloudDatabase.fetchAllSubscriptions { subscriptions, error in guard let

    subscriptions = subscriptions else { return } guard subscriptions.isEmpty else { return } let predicate = NSPredicate(value: true) let querySubscription = CKQuerySubscription( recordType: "Notes", predicate: predicate, options: [ .firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion ]) let notification = CKNotificationInfo() notification.alertBody = "A note was changed or deleted" notification.shouldBadge = true notification.shouldSendContentAvailable = true querySubscription.notificationInfo = notification self.privateCloudDatabase.save(querySubscription) { subscription, error in // Check Error } } Set for push the notification
  38. Fetch sever changes

  39. Fetch sever changes • On app launch • On push

    from CloudKit
  40. Fetch sever changes • On app launch • On push

    from CloudKit
  41. Fetch sever changes var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords:

    [CKRecordID: CKRecord] = [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken }
  42. Fetch sever changes var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords:

    [CKRecordID: CKRecord] = [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Assign serverChangeToken for getting the records from the previous point
  43. Fetch sever changes var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords:

    [CKRecordID: CKRecord] = [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken }
  44. Fetch sever changes var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords:

    [CKRecordID: CKRecord] = [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken }
  45. Fetch sever changes • On app launch • On push

    from CloudKit
  46. Fetch sever changes let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) guard notification.notificationType

    == CKNotificationType.query { return } guard let queryNotification = notification as? CKQueryNotification else { return } guard let recordID = queryNotification.recordID else { return } switch queryNotification.queryNotificationReason { case .recordCreated: // check whether data is duplicate // save data to CoreData case .recordUpdated: // get the data from CoreData // save data to CoreData case .recordDeleted: // get the data from CoreData // delete data from CoreData }
  47. Fetch sever changes let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) guard notification.notificationType

    == CKNotificationType.query { return } guard let queryNotification = notification as? CKQueryNotification else { return } guard let recordID = queryNotification.recordID else { return } switch queryNotification.queryNotificationReason { case .recordCreated: // check whether data is duplicate // save data to CoreData case .recordUpdated: // get the data from CoreData // save data to CoreData case .recordDeleted: // get the data from CoreData // delete data from CoreData } optional public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Swift.Void) •AppKit •UIKit optional public func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any])
  48. Save server changes and token

  49. • Delete records from Core Data • Use deletedRecordIDs variable

    • Categorize records for updating or creating • Use changedRecords variable • Update records to Core Data • Create records to Core Data • Finally save serverChangetoken Save server changes and token
  50. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Save server changes and token
  51. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Prepare variables Save server changes and token
  52. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Append the record or recordID Save server changes and token
  53. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Save server changes and token
  54. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } Save server changes and token
  55. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } • Delete records from Core Data • Use deletedRecordIDs variable • Categorize records for updating or creating • Use changedRecords variable • Update records to Core Data • Create records to Core Data • Finally save serverChangetoken Save server changes and token
  56. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } • Delete records from Core Data • Use deletedRecordIDs variable • Categorize records for updating or creating • Use changedRecords variable • Update records to Core Data • Create records to Core Data • Finally save serverChangetoken Save server changes and token xxxxxxxxxxxxxxx
  57. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } • Delete records from Core Data • Use deletedRecordIDs variable • Categorize records for updating or creating • Use changedRecords variable • Update records to Core Data • Create records to Core Data • Finally save serverChangetoken Send local changes Save server changes and token
  58. Track local change

  59. Offline The application must be available in offline, too.

  60. Track local change

  61. Track local change • Should implement in ourself • Need

    to save the deleted record • Need to fetch records from the previous point • LastSyncedTime: Date • Save to UserDefaults
  62. Track local change • On app launch • On available

    to network
  63. Track local change • Subscribe to network reachability let reachability

    = Reachability()! NotificationCenter.default.addObserver(self, selector: #selector(self.reachabilityChanged), name: ReachabilityChangedNotification, object: reachability ) try! reachability.startNotifier() func reachabilityChanged(note: Notification) { let reachability = note.object as! Reachability if reachability.isReachable { // send local change } } In AppDelegate with the Reachability
  64. var recordZone = CKRecordZone(zoneName: "NotesZone") var changedRecords: [CKRecordID: CKRecord] =

    [:] var deletedRecordIDs = Set<CKRecordID>() let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken let operation = CKFetchRecordZoneChangesOperation( recordZoneIDs: [recordZone.zoneID], optionsByRecordZoneID: [recordZone.zoneID : options] ) operation.recordChangedBlock = { record in changedRecords[record.recordID] = record } operation.recordWithIDWasDeletedBlock = { recordID, string in deletedRecordIDs.insert(recordID) changedRecords.removeValue(forKey: recordID) } operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // save token self.serverChangeToken = serverChangeToken } • Delete records from Core Data • Use deletedRecordIDs variables • Categorize records for updating or creating • Use changedRecords variables • Update records to Core Data • Create records to Core Data • Finally save serverChangetoken Save server changes and token Send local changes
  65. Send local change // Delete Records from iCloud Server let

    operation = CKModifyRecordsOperation( recordsToSave: nil, recordIDsToDelete: recordIDs ) operation.modifyRecordsCompletionBlock = { (records, recordIDs, error) in if let error = error { failure(error) return } // Delete Core Data } privateCloudDatabase.add(operation) • Delete records from Core Data and iCloud Server
  66. Send local change let predicate = NSPredicate(format: "modifiedAt > %@",

    lastSyncedTime) query.predicate = predicate items.forEach { note in if note.recordID != nil { note.update() } else { note.create() } } • Fetch records from Core Data • Save(create/update) records from iCloud server
  67. Send local change let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken

    (…snip…) operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // Delete Record self.sendDeletedRecord { // Fetch and save the records created and modified from the previous point self.sendRecordChanges { // Save the last synced time self.lastSyncedTime = Date() } } self.serverChangeToken = serverChangeToken } self.privateCloudDatabase.add(operation) Fetch/Save server changes
  68. Send local change let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = serverChangeToken

    (…snip…) operation.recordZoneFetchCompletionBlock = { recordZoneID, serverChangeToken, data, result, error in (…snip…) // Delete Record self.sendDeletedRecord { // Fetch and save the records created and modified from the previous point self.sendRecordChanges { // Save the last synced time self.lastSyncedTime = Date() } } self.serverChangeToken = serverChangeToken } self.privateCloudDatabase.add(operation) Send local changes Fetch/Save server changes
  69. Conflict

  70. Conflict • CKRecordSavePolicy • ifServerRecordUnchanged • changedKeys • allKeys

  71. Conflict • CKRecordSavePolicy • ifServerRecordUnchanged default • changedKeys • allKeys

  72. Wrup up

  73. Sync • Subscribe to changes • Fetch server changes •

    Save server changes and token • Send changes to the server • Track local changes • Resolve conflicts
  74. None
  75. Thanks.

  76. None