Last active
August 10, 2022 12:01
-
-
Save roymckenzie/f0c70e48b603a495c458bdd1cfc8ec02 to your computer and use it in GitHub Desktop.
Basic Subscription and Fetch manager and setup for CloudKit
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 UIKit | |
import CloudKit | |
import RealmSwift | |
/// Handles common methods for subscriptions across multiple databases | |
enum CloudKitDatabaseSubscription: String { | |
case `private` | |
case `public` | |
} | |
extension CloudKitDatabaseSubscription { | |
var database: CKDatabase { | |
switch self { | |
case .private: | |
return CKContainer.default().privateCloudDatabase | |
case .public: | |
return CKContainer.default().publicCloudDatabase | |
} | |
} | |
var subscription: CKSubscription { | |
return CKDatabaseSubscription(subscriptionID: subscriptionID) | |
} | |
var subscriptionID: String { | |
return "\(rawValue)SubscriptionIDKey" | |
} | |
var changeToken: CKServerChangeToken? { | |
return UserDefaults.standard.object(forKey: changeTokenKey) as? CKServerChangeToken | |
} | |
var saved: Bool { | |
return UserDefaults.standard.bool(forKey: savedSubscriptionKey) | |
} | |
func set(_ changeToken: CKServerChangeToken?) { | |
UserDefaults.standard.set(changeToken, forKey: changeTokenKey) | |
} | |
func saved(_ saved: Bool) { | |
UserDefaults.standard.set(saved, forKey: savedSubscriptionKey) | |
} | |
private var changeTokenKey: String { | |
return "\(rawValue)DatabaseChangeTokenKey" | |
} | |
private var savedSubscriptionKey: String { | |
return "\(rawValue)SavedSubscriptionKey" | |
} | |
} | |
// In your app delegate add the following code to subscribe to database | |
// changes and to fetch changes when recieving a notification. | |
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
func application(_ application: UIApplication, | |
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { | |
// Creates a private database subscription to listen for changes | |
if !CloudKitDatabaseSubscription.private.saved { | |
CloudKitSyncManager.create(databaseSubscription: .private) | |
} | |
// Register for notifications | |
registerForNotifications(application: application) | |
return true | |
} | |
func application(_ application: UIApplication, | |
didReceiveRemoteNotification userInfo: [AnyHashable : Any], | |
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void){ | |
let cloudKitNotification = CKNotification(fromRemoteNotificationDictionary: userInfo) | |
guard let subscriptionID = cloudKitNotification.subscriptionID else { | |
print("Received a remote notification for unknown subscriptionID") | |
return | |
} | |
switch subscriptionID { | |
case CloudKitDatabaseSubscription.private.rawValue: | |
CloudKitSyncManager.fetchChanges(for: .private) | |
case CloudKitDatabaseSubscription.public.rawValue: | |
CloudKitSyncManager.fetchChanges(for: .public) | |
default: break | |
// WHATEVER I DON'T EVEN KNOW WHO YOU ARE | |
} | |
} | |
} | |
extension AppDelegate { | |
func registerForNotifications(application: UIApplication) { | |
if #available(iOS 10.0, *) { | |
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { success, error in | |
if let error = error { | |
print("Error registering for notifications: \(error)") | |
} | |
if success { | |
print("Successfully registered for notifications") | |
application.registerForRemoteNotifications() | |
} | |
} | |
} else { | |
let settings = UIUserNotificationSettings(types: [.alert], categories: nil) | |
application.registerUserNotificationSettings(settings) | |
application.registerForRemoteNotifications() | |
} | |
} | |
} | |
/// Provides a place to store and retrieve `CKServerChangeToken` objects related to a `CKRecordZone` | |
struct RecordZoneChangeTokenProvider { | |
private static var changeTokenKey: (CKRecordZoneID) -> String = { recordZoneID in | |
return "\(recordZoneID.zoneName)ChangeTokenKey" | |
} | |
static func getChangeToken(for recordZoneID: CKRecordZoneID) -> CKServerChangeToken? { | |
return UserDefaults.standard.object(forKey: changeTokenKey(recordZoneID)) as? CKServerChangeToken | |
} | |
static func set(_ changeToken: CKServerChangeToken?, for recordZoneID: CKRecordZoneID) { | |
UserDefaults.standard.set(changeToken, forKey: changeTokenKey(recordZoneID)) | |
} | |
} | |
// CloudKitSyncManager provides VERY BASIC methods to get you started pulling information | |
// after receiving a remote notification. | |
struct CloudKitSyncManager { | |
// Create a database subscription to receive notifications for changes | |
static func create(databaseSubscription: CloudKitDatabaseSubscription) { | |
// Don't save if it's already been saved | |
// Server will throw error | |
if databaseSubscription.saved { return } | |
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [databaseSubscription.subscription], | |
subscriptionIDsToDelete: nil) | |
operation.modifySubscriptionsCompletionBlock = { subscriptions, _, error in | |
if let error = error { | |
print("Error saving subscription: \(error.localizedDescription)") | |
return | |
} | |
databaseSubscription.saved(true) | |
} | |
databaseSubscription | |
.database | |
.add(operation) | |
} | |
// Call in AppDelegate when | |
// application(_:didReceiveRemoteNotification:fetchCompletionHandler:) | |
static func fetchChanges(for subscription: CloudKitDatabaseSubscription) { | |
// Create operation to fetch database changes | |
// Include previous changeToken if available | |
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: subscription.changeToken) | |
// Record Zone IDs to fetch updates for | |
var recordZoneIDs = [CKRecordZoneID]() | |
// Block called when a Record Zone is returned that has changes | |
operation.recordZoneWithIDChangedBlock = { recordZoneID in | |
recordZoneIDs.append(recordZoneID) | |
} | |
operation.fetchDatabaseChangesCompletionBlock = { changeToken, _, error in | |
subscription.set(changeToken) | |
} | |
subscription.database.add(operation) | |
} | |
// Fetch record changes for multiple recordZoneIDs | |
static func fetchChanges(for recordZoneIDs: [CKRecordZoneID]) { | |
if recordZoneIDs.isEmpty { return } | |
var recordsToSave = [CKRecord]() | |
var recordIDsToDelete = [CKRecordID]() | |
var recordZoneIDsToTryAgain = [CKRecordZoneID]() | |
// Create options for each record zone so that you can use a changeToken | |
var optionsByRecordZone = [CKRecordZoneID : CKFetchRecordZoneChangesOptions]() | |
for recordZoneID in recordZoneIDs { | |
let options = CKFetchRecordZoneChangesOptions() | |
options.previousServerChangeToken = RecordZoneChangeTokenProvider.getChangeToken(for: recordZoneID) | |
return optionsByRecordZone[recordZoneID] = options | |
} | |
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: recordZoneIDs, | |
optionsByRecordZoneID: optionsByRecordZone) | |
// Add changed record to records array for processing at completion | |
operation.recordChangedBlock = { record in | |
recordsToSave.append(record) | |
} | |
// Add deleted recrod ID to array for processing at completion | |
operation.recordWithIDWasDeletedBlock = { recordID, _ in | |
recordIDsToDelete.append(recordID) | |
} | |
// Save record zone changes | |
operation.recordZoneChangeTokensUpdatedBlock = { recordZoneID, changeToken, _ in | |
RecordZoneChangeTokenProvider.set(changeToken, for: recordZoneID) | |
} | |
// Called when an individual recordZone has completed | |
operation.recordZoneFetchCompletionBlock = { recordZoneID, changeToken, _, moreComing, error in | |
if let error = error { | |
print("Error fetching changes for record zone: \(error.localizedDescription)") | |
} | |
if moreComing { | |
RecordZoneChangeTokenProvider.set(changeToken, for: recordZoneID) | |
recordZoneIDsToTryAgain.append(recordZoneID) | |
} | |
} | |
// Called when all the changes have been fetched | |
operation.fetchRecordZoneChangesCompletionBlock = { error in | |
if let error = error { | |
print("Error fetching Record Zone Changes: \(error.localizedDescription)") | |
} | |
// Let's still process changes we did receive even if there was an error | |
updateRealm(recordsToSave: recordsToSave, recordIDsToDelete: recordIDsToDelete) | |
fetchChanges(for: recordZoneIDsToTryAgain) | |
} | |
} | |
/// Update Realm with local | |
static func updateRealm(recordsToSave: [CKRecord]?, recordIDsToDelete: [CKRecordID]?) { | |
// Decode CKRecords to your local Realm objects | |
// Delete Realm objects with matching recordID names | |
// Saves here will trigger `addNotificationBlock` blocks in places | |
// where you are subscribed to Realm notifications | |
do { | |
let realm = try Realm() | |
try realm.write { | |
// realm.save(objects) | |
} | |
} catch { | |
print("Error setting up Realm \(error.localizedDescription)") | |
} | |
} | |
} | |
import UIKit | |
import RealmSwift | |
final class DataDisplayViewController: UIViewController { | |
private var realmNotificationToken: NotificationToken? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
startRealmNotification() | |
} | |
private func startRealmNotification() { | |
do { | |
let realm = try Realm() | |
realmNotificationToken = realm.addNotificationBlock() { [weak self] _, _ in | |
// TODO:- UPDATE UI | |
} | |
} catch { | |
print("Error setting up Realm Notification: \(error.localizedDescription)") | |
} | |
} | |
deinit { | |
realmNotificationToken?.stop() | |
} | |
} |
You need to add the notificationInfo to a subscription otherwise it will throw an error when saving it to iCloud
var subscription: CKSubscription {
let sub = CKDatabaseSubscription(subscriptionID: subscriptionID)
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
sub.notificationInfo = notificationInfo
return sub
}
In the switch statement in line 85 shouldn't access the rawValue. It should look something like this:
switch subscriptionID {
case CloudKitDatabaseSubscription.private.subscriptionID:
CloudKitSyncManager.fetchChanges(for: .private)
case CloudKitDatabaseSubscription.public.subscriptionID:
CloudKitSyncManager.fetchChanges(for: .public)
default: break
// WHATEVER I DON'T EVEN KNOW WHO YOU ARE
}
"You need to add the notificationInfo to a subscription otherwise it will throw an error when saving it to iCloud"
Tell me where it should be added specifically, am I confused?
When user "B" writes changes to the sharedDB, then changes to existing records are made, but adding new and deleting old records does not occur. Gives the error "Couldn't fetch some items when fetching changes"
PS
I have my own code, but the error is similar to the one you pointed out about adding notificationInfo to a subscription.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is great! Thank you for sharing.
You could make a pod out of it.