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