Skip to content

Instantly share code, notes, and snippets.

@Iomegan
Last active October 23, 2019 17:28
Show Gist options
  • Save Iomegan/c633251dc5ac18a983a4a4a48acd599b to your computer and use it in GitHub Desktop.
Save Iomegan/c633251dc5ac18a983a4a4a48acd599b to your computer and use it in GitHub Desktop.
CloudKit Example Controller (macOS 10.12+)
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)
}
}
@DragonAngel1st
Copy link

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?

@Vaberer
Copy link

Vaberer commented Dec 10, 2017

good point, changing a token to nil isn't a good technique

@MrFuFuFu
Copy link

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