I have a very odd bug in my app. Attempting to save an event using saveEvent causes the app to continue in one of 3 ways:
Everything gets saved correctly and without issues
The app crashes with a unrecognized selector sent to instance error, where the offending selector is constraints: and the object to which it's sent is always different and rather unpredictable (they are almost always private SDK classes)
The app crashes with EXC_BAD_ACCESS error
In trying to debug this, I've stripped the app to just the view controller listing the events, with a button to add a new one. The first time I present the view controller to add an event, everything goes smoothly, but the second time I do this, it throws an error.
Here is the code I use:
self.event = EKEvent(eventStore: self.eventStore!)
self.event!.calendar = self.calendar!
self.event!.startDate = self.defaultStartDate()
self.event!.endDate = self.event!.startDate.dateByAddingTimeInterval(3600)
var error: NSError?
self.eventStore!.saveEvent(self.event!, span:EKSpanThisEvent, error: &error)
if let e = error {
println("Saving error: \(error)")
}
If the values for calendar, startDate or endDate are invalid, I get a descriptive error with no crash, but here it crashes at the self.eventStore!.saveEvent(). Any help is appreciated!
Edit
Turns out it was due to an extraneous call to self.eventStore.reset().
After a long search I find the solution.
You have to save your events in background embedding code on a dispatch_async block.
enum UWCalendarError: Int {
case AlreadyExists
case Generic
case NotGranted
}
class Calendar {
static func saveEvent(title: String, startDate: NSDate, duration: NSTimeInterval, completion: (success: Bool, error: UWCalendarError?) -> Void) {
if Calendar.isEventAlreadyScheduled(title, startDate: startDate, duration: duration) {
completion(success: false, error: .AlreadyExists)
} else {
dispatch_async(dispatch_get_main_queue(),{
let store = EKEventStore()
store.requestAccessToEntityType(EKEntityTypeEvent) {(granted, error) in
if !granted {
completion(success: false, error: .NotGranted)
}
var event = EKEvent(eventStore: store)
event.title = title
event.startDate = startDate
event.endDate = event.startDate.dateByAddingTimeInterval(duration)
event.calendar = store.defaultCalendarForNewEvents
var err: NSError?
store.saveEvent(event, span: EKSpanThisEvent, commit: true, error: &err)
if err == nil {
completion(success: true, error: nil)
} else {
completion(success: false, error: .Generic)
}
}
})
}
}
static func isEventAlreadyScheduled(title: String, startDate: NSDate, duration: NSTimeInterval) -> Bool {
let endDate = startDate.dateByAddingTimeInterval(duration)
let eventStore = EKEventStore()
let predicate = eventStore.predicateForEventsWithStartDate(startDate, endDate: endDate, calendars: nil)
let events = eventStore.eventsMatchingPredicate(predicate)
if events == nil {
return false
}
for eventToCheck in events {
if eventToCheck.title == title {
return true
}
}
return false
}
}
Related
So my goal is to always have a proper increment value when a ticket is purchased. I'm having a strange bug where when I update a field value by incrementing it, it increments by the wrong number the first time every time I run a fresh new simulation due to the repeats in print statements, but if I was to update the value again in the same fresh simulation, it will increment by the proper value every time after that.
The value I use to increment is a label text which is dependent on a UIStepper value.
Here is the complete function I use to update and increment this value:
func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: #escaping (PKPaymentAuthorizationResult) -> Void) {
guard let count = guestNumberCount.text else { return }
guard let labelAsANumber = Int64(count) else { return }
guard let user = Auth.auth().currentUser else { return }
let dfmatter = DateFormatter()
dfmatter.dateFormat = "EEEE, MMMM d yyyy, h:mm a"
let dateFromString = dfmatter.date(from: actualDateOfEvent.text ?? "")
let dateStamp: TimeInterval = dateFromString!.timeIntervalSince1970
let dateSt: Int = Int(dateStamp)
getStudentID { (studid) in
if let id = studid {
self.db.document("student_users/\(user.uid)/events_bought/\(self.navigationItem.title!)").setData(["event_name": self.navigationItem.title!, "event_date": self.actualDateOfEvent.text ?? "", "event_cost": self.actualCostOfEvent.text ?? "", "for_grades": self.gradeOfEvent , "school_id": id, "expiresAt":dateSt, "isEventPurchased": true, "time_purchased": Date()], merge: true) { (error) in
if let error = error {
print("There was an error trying to add purchase info to the database: \(error)")
} else {
print("The purchase info was successfully stored to the database!")
}
}
}
}
getDocumentIDOfSelectedEvent { (eventidentification) in
if let eventid = eventidentification {
self.getSchoolDocumentID { (docID) in
if let doc = docID {
self.db.document("school_users/\(doc)/events/\(eventid)/extraEventInfo/TicketCount").updateData(["ticketsPurchased": FieldValue.increment(Int64(labelAsANumber))]) { (error) in
if let error = error {
print("There was an error updating the number of tickets: \(error)")
} else {
print("Number of tickets purchased succesfully updated!")
print(labelAsANumber)
}
}
}
}
}
}
performSegue(withIdentifier: Constants.Segues.fromTicketFormToPurchaseDetails, sender: self)
completion(PKPaymentAuthorizationResult(status: .success, errors: []))
}
I can't really figure out any issues within the function, neither within my whole project as well in regards to this situation other than the print statement. In the print(labelAsANumber) statement, it prints the correct value of the label text (stepper value), but it prints it four times, which is causing the issue. How can I prevent this repetition of print statements and overall incorrect numeric incrementing?
I am trying to read all calendar events from the EventStore. The routine I use, works sometimes but not always.
func getCalendarEvents(_ anfangOpt: Date?, _ endeOpt: Date?) -> [EKEvent]? {
guard let anfang = anfangOpt, let ende = endeOpt else { return nil }
var events: [EKEvent]? = nil
let eventStore = EKEventStore()
eventStore.requestAccess( to: EKEntityType.event, completion: { _,_ in })
if EKEventStore.authorizationStatus(for: EKEntityType.event) == EKAuthorizationStatus.authorized {
var predicate: NSPredicate? = nil
predicate = eventStore.predicateForEvents(withStart: anfang, end: ende, calendars: nil)
if let aPredicate = predicate {
events = eventStore.events(matching: aPredicate)
}
}
return events
}
This function always returns the events. But they are sometimes incomplete. So that
for event in bereinigteEvents {
if dateInInterval(prüfdatum, start: event.startDate, ende: event.endDate) {
istimurlaub = true
if let zwischenname = event.title {
eventname = zwischenname
} else {
eventname = "n/a"
}
eventcalendar = event.calendar.title
trigger.append ("Auslöser: „" + eventname + "“ im Kalender „" + eventcalendar + "“")
}
}
sometimes crashes at the line "eventcalendar = event.calendar.title" and the error message that "nil" was unexpectedly found.
Thank you!
After the first answer I have changed the function, which gets the events to:
func getCalendarEvents(_ anfangOpt: Date?, _ endeOpt: Date?) -> [EKEvent]? {
guard let anfang = anfangOpt, let ende = endeOpt else { return nil }
var events: [EKEvent]? = nil
let eventStore = EKEventStore()
func fetchEvents() {
var predicate: NSPredicate? = nil
predicate = eventStore.predicateForEvents(withStart: anfang, end: ende, calendars: nil)
if let aPredicate = predicate {
events = eventStore.events(matching: aPredicate)
}
}
if EKEventStore.authorizationStatus(for: EKEntityType.event) == EKAuthorizationStatus.authorized {
fetchEvents()
} else {
eventStore.requestAccess( to: EKEntityType.event, completion: {(granted, error) in
if (granted) && (error == nil) {
fetchEvents()
}
})
}
return events
}
But it still crashes with "unexpectedly found nil" in "event.calendar.title".
I ended up using this
Swift 4 How to get all events from calendar?
routine to fetch the events.
The problem still occurs sometimes (!!): Occasionally "nil" is found in "event.calender.title", although it shouldn't be "nil"
The line
eventStore.requestAccess( to: EKEntityType.event, completion: { _,_ in })
is pointless because it works asynchronously. The result of the request is returned after the authorizationStatus check in the next line.
I recommend to first check the status. If the access is not granted ask for permission and perform the fetch. If it's granted perform the fetch directly. This can be accomplished by moving the code to fetch the events into a method.
Note:
It seems that you want to fetch the events when calling the method. Why do you declare start and end date as optional and check for nil?
Declare
func getCalendarEvents(_ anfang: Date, _ ende: Date) -> [EKEvent]? { ...
then you get notified at compile time whether a parameter is nil.
PS: Deutsche Parameternamen mit Umlauten sehen sehr lustig aus. (German parameter names with umlauts look pretty funny)
Problem was, that event.calendar is actually an optional (which I was not aware of).
if let eventZwischenCal = event.calendar {
eventcalendar = eventZwischenCal.title
} else {
eventcalendar = "n/a"
}
fixes the problem.
I am creating an app that provides workout information. On apple watch i have three labels that actively shows HEARTRATE ,DISTANCE TRAVELLED and CALORIES BURNED. Things work fine with apple watch, however the area where things go bad is when i try to show this data on iphone in realtime.
APPLE WATCH PART :-
I started workout on apple watch and saved it using folling code
func startWorkoutSession() {
// Start a workout session with the configuration
if let workoutConfiguration = configuration {
do {
workoutSession = try HKWorkoutSession(configuration: workoutConfiguration)
workoutSession?.delegate = self
workoutStartDate = Date()
healthStore.start(workoutSession!)
} catch {
// ...
}
}
}
Then i save that workout on apple watch using following code :-
func saveWorkout() {
// Create and save a workout sample
let configuration = workoutSession!.workoutConfiguration
let isIndoor = (configuration.locationType == .indoor) as NSNumber
print("locationType: \(configuration)")
let workout = HKWorkout(activityType: configuration.activityType,
start: workoutStartDate ?? Date(),
end: workoutEndDate ?? Date(),
workoutEvents: workoutEvents,
totalEnergyBurned: totalEnergyBurned,
totalDistance: totalDistance,
metadata: [HKMetadataKeyIndoorWorkout:isIndoor]);
healthStore.save(workout) { success, _ in
if success {
self.addSamples(toWorkout: workout)
}
}
// Pass the workout to Summary Interface Controller
// WKInterfaceController.reloadRootControllers(withNames: ["StopPauseInterfaceController"], contexts: [workout])
if #available(watchOSApplicationExtension 4.0, *){
// Create the route, save it, and associate it with the provided workout.
routeBuilder?.finishRoute(with: workout, metadata: metadata) { (newRoute, error) in
guard newRoute != nil else {
// Handle the error here...
return
}
}
}
else {
// fallback to earlier versions
}
}
Then i added samples to those workouts using following code :-
func addSamples(toWorkout workout: HKWorkout) {
// Create energy and distance samples
let totalEnergyBurnedSample = HKQuantitySample(type: HKQuantityType.activeEnergyBurned(),
quantity: totalEnergyBurned,
start: workoutStartDate!,
end: workoutEndDate ?? Date())
let totalDistanceSample = HKQuantitySample(type: HKQuantityType.distanceWalkingRunning(),
quantity: totalDistance,
start: workoutStartDate!,
end: workoutEndDate ?? Date())
// Add samples to workout
healthStore.add([totalEnergyBurnedSample, totalDistanceSample], to: workout) { (success: Bool, error: Error?) in
if success {
// Samples have been added
}
}
}
IPHONE PART :-
This is where things get worse. I try to access the healthkit store using HKSampleObjectQuery and It just returns static values. P.S - I am fetching healthkit store every one second for latest data using NSTimer.
The code which is called every second is :-
func extractDataFromHealthStore(identifier : HKQuantityTypeIdentifier,completion: #escaping (String) -> ()) {
let heightType = HKSampleType.quantityType(forIdentifier:identifier)!
let distantPastDate = Date.distantPast
let endDate = Date()
let predicate = HKQuery.predicateForSamples(withStart: distantPastDate, end: endDate, options: .strictStartDate)
// Get the single most recent Value
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
let query = HKSampleQuery(sampleType: heightType, predicate: predicate, limit: 1, sortDescriptors: [sortDescriptor]) { (query, results, error) in
if let result = results?.first as? HKQuantitySample{
print("Height => \(result.quantity)")
completion("\(result.quantity)")
}else{
print("OOPS didnt get height \nResults => \(String(describing: results)), error => \(error)")
completion("")
}
}
self.healthStore.execute(query)
}
Please guide Me if my whole approach is wrong, or if it is right then what is the mistake at my end.
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)
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()
}
}