I'm following the 'CloudKit Best Practices' WWDC talk about adding subscriptions, which seems to have changed in iOS10.
The code below returns a 'Success!', however my 'AllChanges' subscription never appears in Subscription Types on CloudKit Dashboard.
I'm on Xcode 8 beta 6.
let subscription = CKDatabaseSubscription(subscriptionID:"AllChanges")
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: [])
operation.modifySubscriptionsCompletionBlock = {
(modifiedSubscriptions: [CKSubscription]?, deletedSubscriptionIDs: [String]?, error: Error?) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("Success!")
}
}
operation.qualityOfService = .utility
privateDatabase.add(operation)
I have had the same problem with CKDatabaseSubscription, as have many others:
https://forums.developer.apple.com/thread/53546
https://forums.developer.apple.com/thread/61267
https://forums.developer.apple.com/thread/64071
https://forums.developer.apple.com/thread/63917
I am listing some caveats first in case they explain your issue:
subscriptions often do not appear in "Developer" CloudKit dashboard (they exist, but are not shown - easiest way to test is rename subscription and see if CloudKit complains about duplicate subscriptions)
push notifications are not sent to Simulator
Solution:
What fixed this for me was to create a custom private zone and save all my data to that zone (works in private databases only). Then I receive push notifications on any changes to that zone.
You will need to create the zone (-after- checking for CKAccountStatus = .available and -before- any record saves):
let operation = CKModifyRecordZonesOperation(recordZonesToSave: [CKRecordZone(zoneName: "MyCustomZone")], recordZoneIDsToDelete: nil)
operation.modifyRecordZonesCompletionBlock = { (savedRecordZones: [CKRecordZone]?, deletedRecordZoneIDs: [CKRecordZoneID]?, error: Error?) in
if let error = error {
print("Error creating record zone \(error.localizedDescription)")
}
}
privateDatabase?.add(operation)
And then use that zone when saving your records:
let record = CKRecord(recordType: "MyRecordType", zoneID: CKRecordZone(zoneName: "MyCustomZone"))
// you can save zone to CKRecordID instead, if you want a custom id
Then skip CKFetchDatabaseChangesOperation (because we already know our zone), and use CKFetchRecordZoneChangesOptions instead:
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = myCachedChangeToken
let operation = CKFetchRecordZoneChangesOperation(
recordZoneIDs: [myCustomZoneId],
optionsByRecordZoneID: [myCustomZoneId: options]
)
operation.fetchAllChanges = true
operation.recordChangedBlock = { (record: CKRecord) -> Void in
... do something
}
operation.recordWithIDWasDeletedBlock = { (recordId: CKRecordID, recordType: String) -> Void in
... do something
}
operation.recordZoneFetchCompletionBlock = { (recordZoneId, changeToken, tokenData, isMoreComing, error) in
if let error = error {
print("Error recordZoneFetchCompletionBlock: \(error.localizedDescription)")
return
}
myCachedChangeToken = changeToken
}
privateDatabase?.add(operation)
Related
I've had a long running problem of receiving this error from HealthKit when attempting to "modify" an HKWorkout by (1) copying an existing workout (2) updating the metadata (3) deleting the "old" workout (selectedWorkout in my code below) then (4) saving the new modified workout. 98% of the time this code works flawlessly, however 2% of the time I will get this error and I end up with duplicate workouts. Am I doing anything wrong?
As an aside I really wish HealthKit would let us modify data so that this dance wasn't necessary.
class func updateMetadataDeleteOldAndSaveNewWorkout(selectedWorkout: HKWorkout, handler: #escaping (Bool,WorkoutManagerError? ) -> Void) {
//configure metadata
// Create a new workout with the old workout's fields and new edited metadata object
let newWorkout = HKWorkout(activityType: selectedWorkout.workoutActivityType, start: selectedWorkout.startDate, end: selectedWorkout.endDate, duration: selectedWorkout.duration, totalEnergyBurned: selectedWorkout.totalEnergyBurned, totalDistance: selectedWorkout.totalDistance, metadata: metadata)
// Delete the old workout
HealthStoreSingleton.sharedInstance.healthStore.delete(selectedWorkout, withCompletion: { (success, error) in
DispatchQueue.main.async {
if let unwrappedError = error {
handler(false, WorkoutManagerError.deleteError(unwrappedError.localizedDescription))
return
}
}
// When delete was successful save the new workout
HealthStoreSingleton.sharedInstance.healthStore.save(newWorkout) { success, error in
DispatchQueue.main.async {
if let unwrappedError = error {
handler(false, WorkoutManagerError.saveError(unwrappedError.localizedDescription))
return
}
if success {
handler(true, nil)
return
} else {
handler(false, WorkoutManagerError.saveError("\(String(describing: error))"))
return
}
}
}
})
This app uses CloudKit and I sync the data to Core Data locally. I believe I have the basics working with one exception. The following code checks for changes in CloudKit and allows me to save to Core Data but I have been unable to limit the downloaded changes to one single recordType (I have two recordTypes, "Patient" and "PatientList"). I thought that CKFetchRecordZoneChangesOptions() should allow filtering by recordType but everything I have read and tried only allows filtering by desiredKeys across all recordTypes, which of course is not useful for my purpose. I can limit the Core Data saves by recordType but that seems to be the wrong approach.
Any guidance would be appreciated. Xcode 8.3.3 iOS10 Swift 3
func checkUpdates() {
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken)
let myZone : CKRecordZone = CKRecordZone(zoneName: "myPatientZone")
var zonesIDs : [CKRecordZoneID] = []
//you ONLY want changes from myPatientZone
//you ONLY allow one custom zone and you ONLY share myPatientZone
operation.recordZoneWithIDChangedBlock = { (zoneID) in
if zoneID == myZone.zoneID {
zonesIDs.append(zoneID)
}//if
}//recordZoneWithIDChangedBlock
operation.changeTokenUpdatedBlock = { (token) in
self.changeToken = token
}
operation.fetchDatabaseChangesCompletionBlock = { (token, more, error) in
if error == nil && !zonesIDs.isEmpty {
self.changeToken = token
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = self.fetchChangeToken
let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zonesIDs, optionsByRecordZoneID: [zonesIDs[0] : options])
fetchOperation.recordChangedBlock = { (record) in
let recordName = record.recordID.recordName
let request : NSFetchRequest<Patient> = Patient.fetchRequest()
request.predicate = NSPredicate(format: "recordName = %#", recordName)
do {
let result = try self.persistentContainer.viewContext.fetch(request)
//if the record is not in the local core data
if result.isEmpty {
let patient = Patient(context: self.persistentContainer.viewContext)
patient.recordName = recordName
patient.firstName = record.object(forKey: "firstname") as? String
//all the other fields here...
try self.persistentContainer.viewContext.save()
} else {
//if the record is in core data but has been updated
let patient = result[0]
//patient.recordName - don't change
patient.firstName = record.object(forKey: "firstname") as?
//all the other fields
}//if result.isEmpty
} catch {
//add the CRS custom error handler
print("Error")
}//do catch
}//fetchOperation
//fetchOperation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
//fetchOperation.recordZoneChangeTokensUpdatedBlock = { (zoneID, token, data) in
database.add(operation)
}//checkUpdates
With the help of a few tutorials I coded my first iCloud App. It is working well in Xcode simulator and on my iPhone and iPad. But as soon as I upload it for TestFlight testing it isn't working anymore.
Here is the whole code for getting and uploading the data. It is a simple one-ViewController Shopping list App which has two arrays: listItems for the current shopping list and shopItems for all items which are added so far. These arrays are stored as string lists in the iCloud recordZone All data are stored locally on the device and in the cloud.
The App is checking the connectivity, the iCloud availability and the fact, if the shopping list was edited while being offline, before it gets the data from iCloud.
// Init all values
var listItems = [String]()
var shopItems = [String]()
var cloudCheck = true
var onlineCheck = true
// Init the user defaults
let defaults = UserDefaults.standard
let privateDatabase = CKContainer.default().privateCloudDatabase
let recordZone = CKRecordZone(zoneName: "ShopListZone")
let predicate = NSPredicate(value: true)
var editedRecord: CKRecord!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
if (reachability?.isReachableViaWiFi)! || (reachability?.isReachableViaWWAN)! {
if isICloudContainerAvailable() && defaults.bool(forKey: "changed") == false {
getCloudData()
}
else if isICloudContainerAvailable() && defaults.bool(forKey: "changed") {
loadOffline()
}
else {
cloudCheck = false
}
} else {
onlineCheck = false
loadOffline()
}
}
// Get the record from iCloud
func getCloudData() {
// Connect to iCloud and fetch the data
let query = CKQuery(recordType: "ShopListData", predicate: predicate)
let operation = CKQueryOperation(query: query)
var myItems = [String]()
var allItems = [String]()
operation.recordFetchedBlock = { record in
myItems = record["ListItems"] as! [String]
allItems = record["ShopItems"] as! [String]
}
operation.queryCompletionBlock = { [unowned self] (cursor, error) in
DispatchQueue.main.async {
if error == nil {
self.listItems = myItems
self.shopItems = allItems
self.tableView.reloadData()
} else {
self.cloudCheck = false
print("iCloud load error: \(String(describing: error?.localizedDescription))")
}
}
}
privateDatabase.add(operation)
cloudCheck = true
}
// Upload and save the record to iCloud
#IBAction func uploadShopListData(_ sender: UIButton) {
// Save the shop list in the user defaults
defaults.set(listItems, forKey: "myItems")
// Set bool if saving while offline
if (reachability?.isReachableViaWiFi)! == false && (reachability?.isReachableViaWWAN)! == false {
defaults.set(true, forKey: "changed")
}
// Save the record
if cloudCheck && onlineCheck {
defaults.set(false, forKey: "changed")
saveRecord()
// Show a short message if records were saved successfully
self.myAlertView(title: "iCloud online", message: NSLocalizedString("Shop list was saved in iCloud.", comment: ""))
} else {
// Show a short message if iCloud isn't available
myAlertView(title: "iCloud offline", message: NSLocalizedString("Shop list was saved on iPhone.", comment: ""))
defaults.set(true, forKey: "changed")
}
}
// Save the shop lists
func saveRecord() {
// Connect to iCloud and start operation
let query = CKQuery(recordType: "ShopListData", predicate: predicate)
privateDatabase.perform(query, inZoneWith: recordZone.zoneID) {
allRecords, error in
if error != nil {
// The query returned an error
OperationQueue.main.addOperation {
print("iCloud save error: \(String(describing: error?.localizedDescription))")
// If there is now record yet, create a new one
self.createRecord()
}
} else {
// The query returned the records
if (allRecords?.count)! > 0 {
let newLists = allRecords?.first
newLists?["ListItems"] = self.listItems as CKRecordValue
newLists?["ShopItems"] = self.shopItems as CKRecordValue
self.privateDatabase.save(newLists!, completionHandler: { returnRecord, error in
if error != nil {
// Print an error message
OperationQueue.main.addOperation {
print("iCloud save error: \(String(describing: error?.localizedDescription))")
}
} else {
// Print a success message
OperationQueue.main.addOperation {
print("Shop list was saved successfully")
}
}
})
}
}
}
}
// Create a new record
func createRecord() {
let myRecord = CKRecord(recordType: "ShopListData", zoneID: (self.recordZone.zoneID))
let operation = CKModifyRecordsOperation(recordsToSave: [myRecord], recordIDsToDelete: nil)
myRecord.setObject(self.listItems as CKRecordValue?, forKey: "ListItems")
myRecord.setObject(self.shopItems as CKRecordValue?, forKey: "ShopItems")
operation.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error {
print("iCloud create error: \(String(describing: error.localizedDescription))")
} else {
DispatchQueue.main.async {
print("Records are saved successfully")
}
self.editedRecord = myRecord
}
}
self.privateDatabase.add(operation)
// Show a short message if icloud save was successfull
self.myAlertView(title: "iCloud online", message: NSLocalizedString("Shop list was saved in iCloud.", comment: ""))
}
Any idea, what did I wrong? I read in another post that I should change the iCloud dashboard from development to production, but others say that this should be done only when the App is already on the way to the App store ..
When you upload your app to TestFlight, it's done using live configuration and app will try to connect to production container.
You have two options to test on device:
1) deploy your iCloud schema to production. As you have nothing released yet, it will not break anything.
2) after you archive your project, export it for 'Development deployment'. You will be asked what iCloud Container Environment should be used. You can select 'Development' and install app locally using Apple Configurator
It was not clear to me that setting iCloud Dashboard to production means, that the record type is used, but the record zone I created during development not.
So I have to create the custom record zone first in the createRecord() method (see above). That's it.
I'm experimenting a bit to familiarize myself with the HKAnchoredObjectQuery and getting results when my app is inactive.
I start the app, switch away to Apple Health, enter a blood glucose result; sometimes the results handler is called right away (as evidenced by the print to the console) but other times the handler isn't called until I switch back to my app. Same is true for deleted results as well as added results. Anybody have any guidance?
Most of this code is from a question from thedigitalsean adapted here to get updates while app is in the background and logging to the console. See: Healthkit HKAnchoredObjectQuery in iOS 9 not returning HKDeletedObject
class HKClient : NSObject {
var isSharingEnabled: Bool = false
let healthKitStore:HKHealthStore? = HKHealthStore()
let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
override init(){
super.init()
}
func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) {
let dataTypesToRead : Set<HKObjectType> = [ glucoseType ]
if(!HKHealthStore.isHealthDataAvailable())
{
// let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"])
self.isSharingEnabled = false
return
}
self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in
self.isSharingEnabled = true
authorizationCompleted(success: success, error: error)
}
}
func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:uint, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!) {
let queryEndDate = NSDate(timeIntervalSinceNow: NSTimeInterval(60.0 * 60.0 * 24))
let queryStartDate = NSDate.distantPast()
let sampleType: HKSampleType = glucoseType as! HKSampleType
let predicate: NSPredicate = HKAnchoredObjectQuery.predicateForSamplesWithStartDate(queryStartDate, endDate: queryEndDate, options: HKQueryOptions.None)
var hkAnchor: HKQueryAnchor
if(anchor != nil){
hkAnchor = anchor!
} else {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = {
(query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in
var added = [String]()
var deleted = [String]()
if (addedObjects?.count > 0){
for obj in addedObjects! {
let quant = obj as? HKQuantitySample
if(quant?.UUID.UUIDString != nil){
let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! )
let msg : String = (quant?.UUID.UUIDString)! + " " + String(val)
added.append(msg)
}
}
}
if (deletedObjects?.count > 0){
for del in deletedObjects! {
let value : String = del.UUID.UUIDString
deleted.append(value)
}
}
if(callback != nil){
callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError)
}
}
// remove predicate to see deleted objects
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults)
// added - query should be always running
anchoredQuery.updateHandler = onAnchorQueryResults
// added - allow query to pickup updates when app is in backgroun
healthKitStore?.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate) {
(success, error) in
if (!success) {print("enable background error")}
}
healthKitStore?.executeQuery(anchoredQuery)
}
let AnchorKey = "HKClientAnchorKey"
func getAnchor() -> HKQueryAnchor? {
let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor
return anchor
}
func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor)
NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
class ViewController: UIViewController {
let debugLabel = UILabel(frame: CGRect(x: 10,y: 20,width: 350,height: 600))
override func viewDidLoad() {
super.viewDidLoad()
self.view = UIView();
self.view.backgroundColor = UIColor.whiteColor()
debugLabel.textAlignment = NSTextAlignment.Center
debugLabel.textColor = UIColor.blackColor()
debugLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
debugLabel.numberOfLines = 0
self.view.addSubview(debugLabel)
let hk = HKClient()
hk.requestGlucosePermissions(){
(success, error) -> Void in
if(success){
let anchor = hk.getAnchor()
hk.getGlucoseSinceAnchor(anchor, maxResults: 0)
{ (source, added, deleted, newAnchor, error) -> Void in
var msg : String = String()
if(deleted?.count > 0){
msg += "Deleted: \n" + (deleted?[0])!
for s in deleted!{
msg += s + "\n"
}
}
if (added?.count > 0) {
msg += "Added: "
for s in added!{
msg += s + "\n"
}
}
if(error != nil) {
msg = "Error = " + (error?.description)!
}
if(msg.isEmpty)
{
msg = "No changes"
}
debugPrint(msg)
if(newAnchor != nil && newAnchor != anchor){
hk.saveAnchor(newAnchor!)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.debugLabel.text = msg
})
}
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I also added print()'s at the various application state changes. A sample of the console log (this is running on iPhone 6s device from XCode) shows the handler being called sometimes after I entered background but before reentering foreground and other times only after reentering foreground.
app did become active
"No changes"
app will resign active
app did enter background
app will enter foreground
"Added: E0340084-6D9A-41E4-A9E4-F5780CD2EADA 99.0\n"
app did become active
app will resign active
app did enter background
"Added: CEBFB656-0652-4109-B994-92FAA45E6E55 98.0\n"
app will enter foreground
"Added: E2FA000A-D6D5-45FE-9015-9A3B9EB1672C 97.0\n"
app did become active
app will resign active
app did enter background
"Deleted: \nD3124A07-23A7-4571-93AB-5201F73A4111D3124A07-23A7-4571-93AB-5201F73A4111\n92244E18-941E-4514-853F-D890F4551D76\n"
app will enter foreground
app did become active
app will resign active
app did enter background
app will enter foreground
"Added: 083A9DE4-5EF6-4992-AB82-7CDDD1354C82 96.0\n"
app did become active
app will resign active
app did enter background
app will enter foreground
"Added: C7608F9E-BDCD-4CBC-8F32-94DF81306875 95.0\n"
app did become active
app will resign active
app did enter background
"Deleted: \n15D5DC92-B365-4BB1-A40C-B870A48A70A415D5DC92-B365-4BB1-A40C-B870A48A70A4\n"
"Deleted: \n17FB2A43-0828-4830-A229-7D7DDC6112DB17FB2A43-0828-4830-A229-7D7DDC6112DB\n"
"Deleted: \nCEBFB656-0652-4109-B994-92FAA45E6E55CEBFB656-0652-4109-B994-92FAA45E6E55\n"
app will enter foreground
"Deleted: \nE0340084-6D9A-41E4-A9E4-F5780CD2EADAE0340084-6D9A-41E4-A9E4-F5780CD2EADA\n"
app did become active
I suggest using an HKObserverQuery and setting it up carefully.
There is an algorithm that watches how and when you call the "completion" handler of the HKObserverQuery when you have background delivery enabled. The details of this are vague unfortunately. Someone on the Apple Dev forums called it the "3 strikes" rule but Apple hasn't published any docs that I can find on it's behavior.
https://forums.developer.apple.com/thread/13077
One thing I have noticed is that, if your app is responding to a background delivery with an HKObserverQuery, creating an HKAnchoredObjectQuery, and setting the UpdateHandler in that HKAnchoredObjectQuery, this UpdateHandler will often cause multiple firings of the callback. I suspected that perhaps since these additional callbacks are being executed AFTER you have already told Apple that you have completed you work in response to the background delivery, you are calling the completion handler multiple times and maybe they ding you some "points" and call you less often for bad behavior.
I had the most success with getting consistent callbacks by doing the following:
Using an ObserverQuery and making the sure the call of the "completion" handler gets called once and at the very end of your work.
Not setting an update handler in my HKAnchoredObjectQuery when running in the background (helps achieve 1).
Focusing on making my query handlers, AppDelegate, and ViewController are as fast as possible. I noticed that when I reduced all my callbacks down to just a print statement, the callbacks from HealthKit came immediately and more consistently. So that says Apple is definitely paying attention to execution time. So try to statically declare things where possible and focus on speed.
I have since moved on to my original project which uses Xamarin.iOS, not swift, so I haven't kept up with the code I originally posted. But here is an updated (and untested) version of that code that should take these changes into account (except for the speed improvements):
//
// HKClient.swift
// HKTest
import UIKit
import HealthKit
class HKClient : NSObject {
var isSharingEnabled: Bool = false
let healthKitStore:HKHealthStore? = HKHealthStore()
let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)!
override init(){
super.init()
}
func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) {
let dataTypesToRead : Set<HKObjectType> = [ glucoseType ]
if(!HKHealthStore.isHealthDataAvailable())
{
// let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"])
self.isSharingEnabled = false
return
}
self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in
self.isSharingEnabled = true
authorizationCompleted(success: success, error: error)
}
}
func startBackgroundGlucoseObserver( maxResultsPerQuery: Int, anchorQueryCallback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!)->Void {
let onBackgroundStarted = {(success: Bool, nsError : NSError?)->Void in
if(success){
//Background delivery was successfully created. We could use this time to create our Observer query for the system to call when changes occur. But we do it outside this block so that even when background deliveries don't work,
//we will have the observer query working when are in the foreground at least.
} else {
debugPrint(nsError)
}
let obsQuery = HKObserverQuery(sampleType: self.glucoseType as! HKSampleType, predicate: nil) {
query, completion, obsError in
if(obsError != nil){
//Handle error
debugPrint(obsError)
abort()
}
var hkAnchor = self.getAnchor()
if(hkAnchor == nil) {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
self.getGlucoseSinceAnchor(hkAnchor, maxResults: maxResultsPerQuery, callContinuosly:false, callback: { (source, added, deleted, newAnchor, error) -> Void in
anchorQueryCallback(source: self, added: added, deleted: deleted, newAnchor: newAnchor, error: error)
//Tell Apple we are done handling this event. This needs to be done inside this handler
completion()
})
}
self.healthKitStore?.executeQuery(obsQuery)
}
healthKitStore?.enableBackgroundDeliveryForType(glucoseType, frequency: HKUpdateFrequency.Immediate, withCompletion: onBackgroundStarted )
}
func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:Int, callContinuosly:Bool, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!){
let sampleType: HKSampleType = glucoseType as! HKSampleType
var hkAnchor: HKQueryAnchor;
if(anchor != nil){
hkAnchor = anchor!
} else {
hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor))
}
let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = {
(query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in
var added = [String]()
var deleted = [String]()
if (addedObjects?.count > 0){
for obj in addedObjects! {
let quant = obj as? HKQuantitySample
if(quant?.UUID.UUIDString != nil){
let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! )
let msg : String = (quant?.UUID.UUIDString)! + " " + String(val)
added.append(msg)
}
}
}
if (deletedObjects?.count > 0){
for del in deletedObjects! {
let value : String = del.UUID.UUIDString
deleted.append(value)
}
}
if(callback != nil){
callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError)
}
}
let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults)
if(callContinuosly){
//The updatehandler should not be set when responding to background observerqueries since this will cause multiple callbacks
anchoredQuery.updateHandler = onAnchorQueryResults
}
healthKitStore?.executeQuery(anchoredQuery)
}
let AnchorKey = "HKClientAnchorKey"
func getAnchor() -> HKQueryAnchor? {
let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor
return anchor
}
func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor)
NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey)
NSUserDefaults.standardUserDefaults().synchronize()
}
}
So I want to wipe every record for a particular record type every day. So basically, I want the data to be wiped at 12:00 AM so that it will be fresh for the next day. How would I go about doing this? Is this something that I could set up in the CloudKit dashboard or will I have to set this up programmatically?
Deleting records from the dashboard is a lot of work if you need to delete multiple records.
The best workaround is by creating a separate recordType that will contain one record for every day. Then in the records that you want deleted for that day set up a CKReference to that particular day record and set its action to CKReferenceAction.DeleteSelf
After that you only have to remove the day record and all related records will be removed. Removing that one record could easily be done from the dashboard or you could create functionality in your app or you could create a 2nd app for administrative actions.
func deleteAllRecords()
{
let publicDatabase: CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
// fetch records from iCloud, get their recordID and then delete them
var recordIDsArray: [CKRecordID] = []
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDsArray)
operation.modifyRecordsCompletionBlock = {
(savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecordID]?, error: NSError?) in
print("deleted all records")
}
publicDatabase.add(operation)
}
Try something like this:
let publicDb = CKContainer.defaultContainer().publicCloudDatabase
let query = CKQuery(recordType: "RECORD TYPE", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicDb.performQuery(query, inZoneWithID: nil) { (records, error) in
if error == nil {
for record in records! {
publicDb.deleteRecordWithID(record.recordID, completionHandler: { (recordId, error) in
if error == nil {
//Record deleted
}
})
}
}
}
"RECORD TYPE" should be your record type. Hope this helps.
this code able to delete any amount of records
import CloudKit
class iCloudDelete {
private let cloudDB: CKDatabase
private var recordIDsToDelete = [CKRecordID]()
private var onAllQueriesCompleted : (()->())?
public var resultsLimit = 10 // default is 100
init(cloudDB: CKDatabase){
self.cloudDB = cloudDB
}
func delete(query: CKQuery, onComplete: #escaping ()->Void) {
onAllQueriesCompleted = onComplete
add(queryOperation: CKQueryOperation(query: query))
}
private func add(queryOperation: CKQueryOperation) {
queryOperation.resultsLimit = resultsLimit
queryOperation.queryCompletionBlock = queryDeleteCompletionBlock
queryOperation.recordFetchedBlock = recordFetched
cloudDB.add(queryOperation)
}
private func queryDeleteCompletionBlock(cursor: CKQueryCursor?, error: Error?) {
print("-----------------------")
delete(ids: recordIDsToDelete) {
self.recordIDsToDelete.removeAll()
if let cursor = cursor {
self.add(queryOperation: CKQueryOperation(cursor: cursor))
} else {
self.onAllQueriesCompleted?()
}
}
}
private func recordFetched(record: CKRecord) {
print("RECORD fetched: \(record.recordID.recordName)")
recordIDsToDelete.append(record.recordID)
}
private func delete(ids: [CKRecordID], onComplete: #escaping ()->Void) {
let delete = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: ids)
delete.completionBlock = {
onComplete()
}
cloudDB.add(delete)
}
}