Search code examples
iosswiftcloudkitcompletionhandlerbackground-fetch

Correct use of background fetch completion handler


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

Solution

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