Last active
October 23, 2019 17:28
-
-
Save Iomegan/c633251dc5ac18a983a4a4a48acd599b to your computer and use it in GitHub Desktop.
CloudKit Example Controller (macOS 10.12+)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cocoa | |
import CloudKit | |
@NSApplicationMain | |
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { | |
func applicationDidFinishLaunching(_ notification: Notification) { | |
if #available(OSX 10.12, *) { | |
NSApp.registerForRemoteNotifications(matching: []) | |
} | |
} | |
func application(_ application: NSApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { | |
NSLog("didFailToRegisterForRemoteNotificationsWithError - \(error as NSError)") | |
} | |
func application(_ application: NSApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { | |
print("didRegisterForRemoteNotificationsWithDeviceToken") | |
} | |
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { | |
print("didReceiveRemoteNotification") | |
let dict = userInfo as! [String: NSObject] | |
if #available(OSX 10.12, *) { | |
guard let notification:CKDatabaseNotification = CKNotification(fromRemoteNotificationDictionary:dict) as? CKDatabaseNotification else { return } | |
CloudKitController.sharedInstance.fetchChanges(in: notification.databaseScope) { | |
} | |
} | |
} | |
} | |
@available(OSX 10.12, *) | |
public extension UserDefaults { | |
public var sharedDatabaseServerChangeToken: CKServerChangeToken? { | |
get { | |
guard let data = self.value(forKey: "SharedDatabaseServerChangeToken") as? Data else { | |
return nil | |
} | |
guard let token = NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken else { | |
return nil | |
} | |
Swift.print("SharedDatabaseServerChangeToken: \(token)") | |
return token | |
} | |
set { | |
if let token = newValue { | |
let data = NSKeyedArchiver.archivedData(withRootObject: token) | |
self.set(data, forKey: "SharedDatabaseServerChangeToken") | |
} else { | |
self.removeObject(forKey: "SharedDatabaseServerChangeToken") | |
} | |
} | |
} | |
public var publicDatabaseServerChangeToken: CKServerChangeToken? { | |
get { | |
guard let data = self.value(forKey: "PublicDatabaseServerChangeToken") as? Data else { | |
return nil | |
} | |
guard let token = NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken else { | |
return nil | |
} | |
Swift.print("PublicDatabaseServerChangeToken: \(token)") | |
return token | |
} | |
set { | |
if let token = newValue { | |
let data = NSKeyedArchiver.archivedData(withRootObject: token) | |
self.set(data, forKey: "PublicDatabaseServerChangeToken") | |
} else { | |
self.removeObject(forKey: "PublicDatabaseServerChangeToken") | |
} | |
} | |
} | |
public var privateDatabaseServerChangeToken: CKServerChangeToken? { | |
get { | |
guard let data = self.value(forKey: "PrivateDatabaseServerChangeToken") as? Data else { | |
return nil | |
} | |
guard let token = NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken else { | |
return nil | |
} | |
Swift.print("PrivateDatabaseServerChangeToken: \(token)") | |
return token | |
} | |
set { | |
if let token = newValue { | |
let data = NSKeyedArchiver.archivedData(withRootObject: token) | |
self.set(data, forKey: "PrivateDatabaseServerChangeToken") | |
} else { | |
self.removeObject(forKey: "PrivateDatabaseServerChangeToken") | |
} | |
} | |
} | |
public var customZone1ServerChangeToken: CKServerChangeToken? { | |
get { | |
guard let data = self.value(forKey: "CustomZone1ServerChangeToken") as? Data else { | |
return nil | |
} | |
guard let token = NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken else { | |
return nil | |
} | |
Swift.print("CustomZone1ServerChangeToken: \(token)") | |
return token | |
} | |
set { | |
if let token = newValue { | |
let data = NSKeyedArchiver.archivedData(withRootObject: token) | |
self.set(data, forKey: "CustomZone1ServerChangeToken") | |
} else { | |
self.removeObject(forKey: "CustomZone1ServerChangeToken") | |
} | |
} | |
} | |
public var defaultZoneServerChangeToken: CKServerChangeToken? { | |
get { | |
guard let data = self.value(forKey: "DefaulZoneServerChangeToken") as? Data else { | |
return nil | |
} | |
guard let token = NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken else { | |
return nil | |
} | |
Swift.print("DefaulZoneServerChangeToken: \(token)") | |
return token | |
} | |
set { | |
if let token = newValue { | |
let data = NSKeyedArchiver.archivedData(withRootObject: token) | |
self.set(data, forKey: "DefaultZoneServerChangeToken") | |
} else { | |
self.removeObject(forKey: "DefaultZoneServerChangeToken") | |
} | |
} | |
} | |
} | |
@available(OSX 10.12, *) | |
class CloudKitController: AnyObject { | |
private let publicDB = CKContainer.default().publicCloudDatabase | |
private let privateDB = CKContainer.default().privateCloudDatabase | |
private let sharedDB = CKContainer.default().sharedCloudDatabase | |
private let container = CKContainer.default() | |
let privateSubscriptionId = "private-changes" | |
//let sharedSubscriptionId = "shared-changes" //No shareddb in this example | |
// Use a consistent zone ID across the user's devices | |
// CKCurrentUserDefaultName specifies the current user's ID when creating a zone ID | |
let customZone1ID = CKRecordZoneID(zoneName: "CustomZone1", ownerName: CKCurrentUserDefaultName) | |
let createZoneGroup = DispatchGroup() | |
static let sharedInstance = CloudKitController() | |
init() { | |
//Subscripe to changes | |
if !UserDefaults.standard.bool(forKey: "SubscribedToPrivateChanges") { | |
NSLog("Subscripe to private changes") | |
let createSubscriptionOperation = self.createDatabaseSubscriptionOperation(subscriptionId: privateSubscriptionId) | |
createSubscriptionOperation.modifySubscriptionsCompletionBlock = { (subscriptions, deletedIds, error) in | |
if error == nil { UserDefaults.standard.set(true, forKey: "SubscribedToPrivateChanges") } | |
// else custom error handling | |
} | |
self.privateDB.add(createSubscriptionOperation) | |
} | |
// Fetch any changes from the server that happened while the app wasn't running | |
createZoneGroup.notify(queue: DispatchQueue.global()) { | |
if UserDefaults.standard.bool(forKey: "CreatedCustomZone1") { | |
NSLog("Fetch changes from the server that happened while the app wasn't running") | |
self.fetchChanges(in: .private) {} | |
// self.fetchChanges(in: .shared) {} | |
} | |
else { | |
NSLog("Don't fetch changes from the server that happened while the app wasn't running") | |
} | |
} | |
if !UserDefaults.standard.bool(forKey: "CreatedCustomZone1") { | |
createZoneGroup.enter() | |
let customZone1 = CKRecordZone(zoneID: customZone1ID) | |
let createZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [customZone1], recordZoneIDsToDelete: []) | |
createZoneOperation.modifyRecordZonesCompletionBlock = { (saved, deleted, error) in | |
if (error == nil) { UserDefaults.standard.set(true, forKey: "CreatedCustomZone1") } | |
// else custom error handling | |
NSLog("Creating Custom Zone 1 successfull") | |
self.createZoneGroup.leave() | |
} | |
createZoneOperation.qualityOfService = .userInitiated | |
self.privateDB.add(createZoneOperation) | |
} | |
} | |
func createDatabaseSubscriptionOperation(subscriptionId: String) -> CKModifySubscriptionsOperation { | |
let subscription = CKDatabaseSubscription.init(subscriptionID: subscriptionId) | |
let notificationInfo = CKNotificationInfo() | |
// send a silent notification | |
notificationInfo.shouldSendContentAvailable = true | |
subscription.notificationInfo = notificationInfo | |
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) | |
operation.qualityOfService = .userInteractive | |
return operation | |
} | |
func fetchChanges(in databaseScope: CKDatabaseScope, completion: @escaping () -> Void) { | |
switch databaseScope { | |
case .private: | |
fetchDatabaseChanges(database: self.privateDB, databaseTokenKey: "private", completion: completion) | |
case .shared: | |
fetchDatabaseChanges(database: self.sharedDB, databaseTokenKey: "shared", completion: completion) | |
case .public: | |
fatalError() | |
} | |
} | |
func fetchDatabaseChanges(database: CKDatabase, databaseTokenKey: String, completion: @escaping () -> Void) { | |
var changedZoneIDs: [CKRecordZoneID] = [] | |
var changeToken: CKServerChangeToken? | |
if database == privateDB { | |
changeToken = UserDefaults.standard.privateDatabaseServerChangeToken // Read change token from disk | |
} | |
else if database == publicDB { | |
changeToken = UserDefaults.standard.publicDatabaseServerChangeToken // Read change token from disk | |
} | |
else if database == sharedDB { | |
changeToken = UserDefaults.standard.sharedDatabaseServerChangeToken // Read change token from disk | |
} | |
else { | |
NSLog("ERROR: #3duNU§ \(database)") | |
fatalError() | |
} | |
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken) | |
operation.recordZoneWithIDChangedBlock = { (zoneID) in | |
changedZoneIDs.append(zoneID) | |
} | |
operation.recordZoneWithIDWasDeletedBlock = { (zoneID) in | |
// Write this zone deletion to memory | |
fatalError() | |
} | |
operation.changeTokenUpdatedBlock = { (token) in | |
// Flush zone deletions for this database to disk | |
if database == self.privateDB { | |
UserDefaults.standard.privateDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else if database == self.privateDB { | |
UserDefaults.standard.publicDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else if database == self.sharedDB { | |
UserDefaults.standard.sharedDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else { | |
NSLog("ERROR: #1xh87hH§ \(database)") | |
fatalError() | |
} | |
} | |
operation.fetchDatabaseChangesCompletionBlock = { (token, moreComing, error) in | |
if let error = error { | |
print("Error during fetch shared database changes operation", error) | |
completion() | |
return | |
} | |
// Flush zone deletions for this database to disk | |
if database == self.privateDB { | |
UserDefaults.standard.privateDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else if database == self.privateDB { | |
UserDefaults.standard.publicDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else if database == self.sharedDB { | |
UserDefaults.standard.sharedDatabaseServerChangeToken = token // Write this new database change token to memory | |
} | |
else { | |
NSLog("ERROR: #Q§D0=ZTdb4§ \(database)") | |
fatalError() | |
} | |
self.fetchZoneChanges(database: database, databaseTokenKey: databaseTokenKey, zoneIDs: changedZoneIDs) { | |
if database == self.privateDB { | |
UserDefaults.standard.privateDatabaseServerChangeToken = nil // Flush in-memory database change token to disk | |
} | |
else if database == self.privateDB { | |
UserDefaults.standard.publicDatabaseServerChangeToken = nil // Flush in-memory database change token to disk | |
} | |
else if database == self.sharedDB { | |
UserDefaults.standard.sharedDatabaseServerChangeToken = nil // Flush in-memory database change token to disk | |
} | |
else { | |
NSLog("ERROR: #3duNU§ \(database)") | |
fatalError() | |
} | |
completion() | |
} | |
} | |
operation.qualityOfService = .userInitiated | |
database.add(operation) | |
} | |
func fetchZoneChanges(database: CKDatabase, databaseTokenKey: String, zoneIDs: [CKRecordZoneID], completion: @escaping () -> Void) { | |
// Look up the previous change token for each zone | |
var optionsByRecordZoneID = [CKRecordZoneID: CKFetchRecordZoneChangesOptions]() | |
for zoneID in zoneIDs { | |
let options = CKFetchRecordZoneChangesOptions() | |
switch zoneID.zoneName { | |
case "CustomZone1": | |
options.previousServerChangeToken = UserDefaults.standard.customZone1ServerChangeToken // Read change token from disk | |
default: | |
options.previousServerChangeToken = UserDefaults.standard.defaultZoneServerChangeToken // Read change token from disk | |
} | |
optionsByRecordZoneID[zoneID] = options | |
} | |
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID) | |
operation.recordChangedBlock = { (record) in | |
print("Record changed:", record) | |
// Write this record change to memory | |
switch record.recordType { | |
case "Testrecord": break | |
default: | |
NSLog("ERROR: #5f67gJ2 unsupported type: \(record.recordType)") | |
} | |
} | |
operation.recordWithIDWasDeletedBlock = { (recordId) in | |
print("Record deleted:", recordId) | |
// Write this record deletion to memory | |
} | |
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in | |
// Flush record changes and deletions for this zone to disk | |
switch zoneId.zoneName { | |
case "CustomZone1": | |
UserDefaults.standard.customZone1ServerChangeToken = token // Write this new zone change token to disk | |
default: | |
UserDefaults.standard.defaultZoneServerChangeToken = token // Write this new zone change token to disk | |
} | |
} | |
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in | |
if let error = error { | |
print("Error fetching zone changes for \(databaseTokenKey) database:", error) | |
return | |
} | |
// Flush record changes and deletions for this zone to disk | |
switch zoneId.zoneName { | |
case "CustomZone1": | |
UserDefaults.standard.customZone1ServerChangeToken = changeToken // Write this new zone change token to disk | |
default: | |
UserDefaults.standard.defaultZoneServerChangeToken = changeToken // Write this new zone change token to disk | |
} | |
} | |
operation.fetchRecordZoneChangesCompletionBlock = { (error) in | |
if let error = error { | |
print("Error fetching zone changes for \(databaseTokenKey) database:", error) | |
} | |
completion() | |
} | |
database.add(operation) | |
} | |
} |
good point, changing a token to nil isn't a good technique
Hi I'm wondering how this part works?
// Fetch any changes from the server that happened while the app wasn't running
createZoneGroup.notify(queue: DispatchQueue.global()) {
if UserDefaults.standard.bool(forKey: "CreatedCustomZone1") {
NSLog("Fetch changes from the server that happened while the app wasn't running")
self.fetchChanges(in: .private) {}
// self.fetchChanges(in: .shared) {}
}
else {
NSLog("Don't fetch changes from the server that happened while the app wasn't running")
}
}
I think any changes from the server will get a notification and didReceiveRemoteNotification
method will received it. So we should handle it on didReceiveRemoteNotification
. The comment says Fetch any changes from the server that happened while the app wasn't running
. I don't even understand how this part works.
Any idea? Thanks
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In self.fetchZoneChanges(database: database, databaseTokenKey: databaseTokenKey, zoneIDs: changedZoneIDs), in its completion handler you send along, you set all your database's local change token to "nil". Why do you do this and won't that make you get all the changes since the beginning of all changes on cloudKit?