Skip to content

Instantly share code, notes, and snippets.

@andreweades
Forked from roymckenzie/CloudKitBasics.swift
Created February 19, 2020 17:07
Show Gist options
  • Save andreweades/58ca9f78424ce8588f908f8ca02e57ae to your computer and use it in GitHub Desktop.
Save andreweades/58ca9f78424ce8588f908f8ca02e57ae to your computer and use it in GitHub Desktop.
Basic Subscription and Fetch manager and setup for CloudKit
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()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment