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. J04%$
    Practical
    CloudKit

    View Slide

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

    View Slide

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

    View Slide

  4. Agenda
    • Background
    • What is CloudKit ?
    • About sync
    • Wrap up

    View Slide

  5. Background

    View Slide

  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

    View Slide

  7. 2014

    View Slide

  8. View Slide

  9. View Slide

  10. What is CloudKit?

    View Slide

  11. What is CloudKit?

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  15. Why

    View Slide

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

    View Slide

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

    View Slide

  18. Core Data iCloud

    View Slide

  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

    View Slide

  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

    View Slide

  21. CloudKit

    View Slide

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

    View Slide

  23. View Slide

  24. Sync

    View Slide

  25. Sync

    View Slide

  26. Sync
    • Subscribe to changes
    • Fetch server changes
    • Save server changes and token
    • Send changes to the server
    • Track local changes
    • Resolve conflicts

    View Slide

  27. Online

    View Slide

  28. Online

    View Slide

  29. Offline

    View Slide

  30. Offline

    View Slide

  31. Subscribe to changes

    View Slide

  32. Subscribe to changes
    • On app launch
    • Prepare for fetching server changes
    • Setup the push notifications for changes

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  38. Fetch sever changes

    View Slide

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

    View Slide

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

    View Slide

  41. Fetch sever changes
    var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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
    }

    View Slide

  42. Fetch sever changes
    var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  43. Fetch sever changes
    var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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
    }

    View Slide

  44. Fetch sever changes
    var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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
    }

    View Slide

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

    View Slide

  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
    }

    View Slide

  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])

    View Slide

  48. Save server changes and token

    View Slide

  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

    View Slide

  50. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  51. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  52. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  53. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  54. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  55. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  56. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  57. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  58. Track local change

    View Slide

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

    View Slide

  60. Track local change

    View Slide

  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

    View Slide

  62. Track local change
    • On app launch
    • On available to network

    View Slide

  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

    View Slide

  64. var recordZone = CKRecordZone(zoneName: "NotesZone")
    var changedRecords: [CKRecordID: CKRecord] = [:]
    var deletedRecordIDs = Set()
    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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  69. Conflict

    View Slide

  70. Conflict
    • CKRecordSavePolicy
    • ifServerRecordUnchanged
    • changedKeys
    • allKeys

    View Slide

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

    View Slide

  72. Wrup up

    View Slide

  73. Sync
    • Subscribe to changes
    • Fetch server changes
    • Save server changes and token
    • Send changes to the server
    • Track local changes
    • Resolve conflicts

    View Slide

  74. View Slide

  75. Thanks.

    View Slide

  76. View Slide