Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Practical CloudKit

nakajijapan
September 17, 2017

Practical CloudKit

iOSDC 2017 day 2

I talked about sync.

nakajijapan

September 17, 2017
Tweet

More Decks by nakajijapan

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. Why

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

    Don’t need to prepare the certificate • A genuine interest
  7. 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
  8. 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
  9. Sync • Subscribe to changes • Fetch server changes •

    Save server changes and token • Send changes to the server • Track local changes • Resolve conflicts
  10. Subscribe to changes • On app launch • Prepare for

    fetching server changes • Setup the push notifications for changes
  11. 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 } }
  12. 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
  13. 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
  14. 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 } }
  15. 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
  16. 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 }
  17. 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
  18. 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 }
  19. 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 }
  20. 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 }
  21. 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])
  22. • 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Sync • Subscribe to changes • Fetch server changes •

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