My app uses CloudKit and I am trying to implement background fetch.
The method in App Delegate calls a method in my main view controller which checks for changes in the CloudKit database.
However, I realise that I am not calling the completion handler correctly, as the closures for the CloudKit will return asynchronously. I am really unsure how best to call the completion handler in the app delegate method once the operation is complete. Can I pass the completion handler through to the view controller method?
App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges()
completionHandler(.newData)
}
}
Main view controller method to get CloudKit changes
// Fetch zone changes (a method in main table view controller)
func getZoneChanges() {
DispatchQueue.global(qos: .userInitiated).async {
let customZone = CKRecordZone(zoneName: "Drugs")
let zoneID = customZone.zoneID
let zoneIDs = [zoneID]
let changeToken = UserDefaults.standard.serverChangeToken // Custom way of accessing User Defaults using an extension
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]()
// Some other functioning code to process options
// CK Zone Changes Operation
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
// Closures for records changed, deleted etc.
// Closure details omitted for brevity as fully functional as expected.
// These closures change data model, Spotlight indexing, notifications and trigger UI refresh etc.
operation.recordChangedBlock = { (record) in
// Code...
}
operation.recordWithIDWasDeletedBlock = { (recordId, string) in
// Code...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
}
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
} else {
print("Changes fetched successfully!")
// Save local items
self.saveData() // Uses NSCoding
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
}
Update your getZoneChanges
to have a completion parameter.
func getZoneChanges(completion: @escaping (Bool) -> Void) {
// the rest of your code
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
completion(false)
} else {
print("Changes fetched successfully!")
completion(true)
}
}
}
Then you can update the app delegate method to use it:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges { (success) in
completionHandler(success ? .newData : .noData)
}
}
}