I am trying to perform some actions triggered by changes to Apple Health Kit, triggered in the background of my Swift iOS app.
Here's my AppDelegate:
var healthManager : HealthManager?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
healthManager = HealthManager.sharedInstance
return true
}
And in the initialization of the HealthManager class I authorize use of Health Kit and call:
var sampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
var predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.StrictStartDate)
var query = HKObserverQuery(sampleType: sampleType, predicate: predicate, updateHandler: stepsChangedHandler)
healthKitStore.executeQuery(query)
healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded, error) in
if succeeded {
println("Enabled background delivery of step changes")
} else {
if let theError = error {
print("Failed to enable background delivery of step changed. ")
println("Error = \(theError)")
}
}
})
This works beautifully when the app is open- the stepsChangedHandler is called when there's a health kit update, but when the app is out of focus it never is called. I've searched around and found a few ideas, but none of the fixes have seemed to work for me.
Thanks!
What you have should work, but my experience with the simulator up through iOS 8.4 and Xcode 6.4 is that the background updates are not triggered. However, in my testing this does work on a device. To try for yourself, hook up and run your app on a device then switch to Health.app and add a relevant data point.
If your query is set for immediate updates, you should see your log message in the console. Make sure stepsChangedHandler includes completionHandler().
According to the docs, the query runs on a separate background thread so your appdelegate code will only be called on initial launch.
In the documentation for the HKHealthStore Class, under enableBackgroundDeliveryForType:... there is a paragraph:
Some data types, such as step counts, have a minimum frequency of HKUpdateFrequencyHourly. This frequency is enforced transparently.
which explains why you won't see background updates as frequently as you are specifying. I'm not sure if theres a listing of which data types are included in the "some" quantifier.
Related
If a method is Deprecated, does this mean it will not work in updated systems?
I was using setMinimumBackgroundFetchInterval for background fetch and I believe it stopped working for ios 13 and newer versions.
Do you think it is because it is deprecated or a bug?
When I use Simulate Background Fetch it works fine, but it does not fetch when I really use the app.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let successCallback: (_ data: Double) -> Void = { (data) in
if data == nil {
// No new data
completionHandler(.noData)
} else {
// There is a new data set
self.bodyMassObserverQueryTriggered() // count todays steps and updates database
completionHandler(.newData)
}
}
getSteps(completion: successCallback) // count todays steps and updates database
}
and I have the following lines in applicationdidfinishwithoptions:
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum)
UIApplication.shared.setMinimumBackgroundFetchInterval(5*60)
Do I need anything else?
EDIT: One thing i found is i needed to remove BGTaskSchedulerPermittedIdentifiers from Info.plist according to apple docs:
Adding a BGTaskSchedulerPermittedIdentifiers key to the Info.plist disables application(:performFetchWithCompletionHandler:) and setMinimumBackgroundFetchInterval(:) in iOS 13 and later.
I need to execute a task when the app is in background state. For example, when the app enters the background state, then every 5 minutes(app is in background in this time) a task is executed.
I tried with location changed but I can't use a precise location(for battery consume) then I used significant location changed but If user doesn't move or doesn't change cell tower location is not updated.
Can you help me about it?
Yo could use the iOS Background Fetch feature where you can specify minimum background fetch interval. But actual interval between successive invocation of your code will be determined by iOS framework.
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let data: String? = nil
do {
//fetch some data
if let data = getSomeNewData() {
/* use the completionHandler to talk to the system and tell us if the app fetched new data, or if no data was available. */
completionHandler(.newData)
} else {
completionHandler(.noData)
}
} catch {
print(error)
completionHandler(.failed)
}
}
see also question: swift-ios-refreshing-app-data-when-in-background
Another option is to setup a server that will send a (silent) push notification to your app every 5 minutes that your app can react to.
I'm trying to make my app synchronize HealthKit data with our database whenever new data input is observed. Despite some ambiguity in the logic I believe I've managed to enable background delivery for the sample types and have the observers react when needed.
However, in the observer's updatehandler I need to create an HKAnchoredObjectQuery for fetching the most recent results from HealthKit, but these queries don't return reliably when my app is running in the background. Often when I add sample points in HealthKit, the anchored queries just get executed, but they return only when I bring the app back in the foreground.
Other times they return immediately without me having to activate the app. One possible issue is that all of my observer queries fire their updatehandlers, thus creating multiple AnchoredObjectQueries that may or may not return. I've explained that further in the linked thread, because it may be unrelated.
Here's an example of the function I'm calling from HKObserverQuery's updatehandler:
func synchronizeRecentData(sampleType: HKSampleType, observerQuery: HKObserverQuery) {
let completionHandler: (HKAnchoredObjectQuery, [HKSample]?, Int, NSError?) -> Void = {
[unowned self] query, results, newAnchor, error in
if error != nil {
abort()
}
// Update queryAnchor
self.queryAnchors[sampleType] = newAnchor
guard let receivedNewSamples = results as? [HKQuantitySample] else {
abort()
}
// Handle received samples here
}
let query = HKAnchoredObjectQuery(type: sampleType,
predicate: nil,
anchor: self.queryAnchors[sampleType]!,
limit: HKObjectQueryNoLimit,
completionHandler: completionHandler)
healthKitStore.executeQuery(query)
}
According to the HealthKit docs:
Unlike the observer query, these updates include a list of items that
have been added or removed; however, anchored object queries cannot be
registered for background delivery. For more information, see
HKAnchoredObjectQuery.
The only query that can register for background delivery is the HKObserverQuery
I believe this is because when your app gets woken up in the background you have very limited time to execute before the app gets suspended again. If you use UIBackgroundTaskIdentifier you should be able to ensure that your app runs long enough to finish getting the results of the HKAnchoredObjectQuery.
I'm trying to make iOS app to communicate with watch, but i get inconsistent behaviour all the time - either the communication is too slow, or none of the data gets transferred at all.
Besides, i don't see any "Phone disabled" screen when the watchKit runs (which causes a crash, because i need to get data from the phone first).
This is what i have in regards to establishing the WCSession in the iPhone app
App Delegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSClassFromString("WCSession") != nil {
if #available(iOS 9.0, *) {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}
} else {
}}
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
session.sendMessage(["b":"delegateSaysHi"], replyHandler: nil, errorHandler: nil)
}}
}
MainViewController
(viewDidLoad)
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}}
MainViewController (Method for transferring bunch of data from iOS app to watchKit app)
func transferData(){
do {
let dataArray = ["somedata": array2d1]
try WCSession.defaultSession().updateApplicationContext(dataArray)
let dataArray1 = ["somedata1": array2d2]
try WCSession.defaultSession().updateApplicationContext(dataArray1)
let dataArray2 = ["somedata2": array2d3]
try WCSession.defaultSession().updateApplicationContext(dataArray2)
let dataArray3 = ["somedata3": array2d4]
try WCSession.defaultSession().updateApplicationContext(dataArray3)
// and up to 12
}
catch {
print("Something wrong happened")
}
}
And this is for watchKit app
App Delegate
func applicationDidFinishLaunching() {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func applicationDidBecomeActive() {
if(WCSession.isSupported()){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
InterfaceController (awakeWithContext)
if(WCSession.defaultSession().reachable){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
Method for receiving ApplicationContext data
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if let retrievedArray1 = applicationContext["somedata"] as? [[String]] {
self.watchAppArray = retrievedArray1
}
if let retrievedArray2 = applicationContext["somedata2"] as? [[String]] {
self.watchAppArray = retrievedArray1
// and so on for 12 arrays sent from phone
}
}
}}
Any advices on clearing out the situation are very welcome!
Thank you.
Multiple delegates/activations:
You're repeatedly setting up, delegating, and activating sessions in different parts of your app. You keep changing your delegate, so code in one part of the app will no longer be used after you delegated handling to a different part of your app.
You should use a single session/delegate throughout your app. One solution is to setup a WCSession singleton which would be available app-wide. Here's a guide which walks you through that process.
Only the most recent application context would get sent:
By trying to queue up multiple application context requests, the earlier ones would no longer be in the queue when the system gets around to transmitting it, as the system would have already replaced the preceding context with the later one. So only the last (dataArray3) would ever get transmitted.
Use the updateApplicationContext:error: method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. ... This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
If all of the arrays represent the recent state of your application, you want to transmit them together in a single dictionary.
var dataArray = [String: AnyObject]()
dataArray["somedata"] = array2d1
dataArray["somedata1"] = array2d2
dataArray["somedata2"] = array2d3
dataArray["somedata3"] = array2d4
do {
try session.updateApplicationContext(dataArray)
}
catch {
print(error)
}
It may also help to add some error handling to your sendMessage code, as the paired device may not always be reachable.
Slow communication:
As for the communication being too slow, there are two issues at hand.
Transfers may not happen immediately.
When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
The more data you send, the longer it takes to transmit/receive it all.
When sending messages, send only the data that your app needs. All transfers involve sending data wireless to the counterpart app, which consumes power. Rather than sending all of your data every time, send only the items that have changed.
You can control how much data you send, as well as whether the data is sent interactively or in the background. If the watch is reachable, you could use sendMessage for immediate communication. If it's not reachable, you could fall back on a background method.
Can HealthKit background delivery launch the application if is not running? Particularly in a terminated state?
After a full day of testing, I can confirm that HealthKit background delivery does work in all of the following application states:
background: in background and executing code,
suspended: in background and not executing code,
terminated: force-killed by the user or purged by the system.
Keep in mind: part 1
Some HealthKit data types have a minimum update frequency of HKUpdateFrequencyHourly. That said, even if you set up a background delivery with frequency HKUpdateFrequencyImmediate, you won't get updates more often than every hour or so.
Unfortunately, there is no info in the documentation about minimum frequencies per data type, but my experience with Fitness types was as follows:
Active Energy: hourly,
Cycling Distance: immediate,
Flights Climbed: immediate,
NikeFuel: immediate,
Steps: hourly,
Walking + Running Distance: hourly,
Workouts: immediate.
Note: immediate DOES NOT mean real-time but rather "some time shortly after" the activity data samples have been written to the HealthKit database/store.
Keep in mind: part 2
If the device is locked with a passcode, none of your background delivery observers will be called. This is intentional, due to privacy.
That said, as soon as a device is unlocked, your HealthKit background delivery observers will be called, given that the minimum frequency time has passed.
Sample code
Take a look at Viktor Sigler's answer, but you can skip all three steps from the beginning of his answer since they are not required for HealthKit background delivery to work.
This answer is some late but I hope this help the people to understand how to work with the HKObserverQuery successfully.
First of all the HKObserverQuery works fine in background mode and when the app is closed at all. But you need to set some options first to allow everything works fine.
You need to set the Background Modes in the Capabilities of your app. See below picture:
Then you need to add the Required Background Modes in your info.plist as in the following picture:
You need to set the Background Fetch in the following way:
3.1. From the Scheme toolbar menu, choose an iOS Simulator or Device.
3.2. From the same menu, choose Edit Scheme.
3.3. In the left column, select Run.
3.4. Select the Options tab.
3.5. Select the Background Fetch checkbox and click Close.
Then you can receive notifications when the app is in background or closed using the following code:
import UIKit
import HealthKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let healthKitStore:HKHealthStore = HKHealthStore()
func startObservingHeightChanges() {
let sampleType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)
var query: HKObserverQuery = HKObserverQuery(sampleType: sampleType, predicate: nil, updateHandler: self.heightChangedHandler)
healthKitStore.executeQuery(query)
healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded: Bool, error: NSError!) in
if succeeded{
println("Enabled background delivery of weight changes")
} else {
if let theError = error{
print("Failed to enable background delivery of weight changes. ")
println("Error = \(theError)")
}
}
})
}
func heightChangedHandler(query: HKObserverQuery!, completionHandler: HKObserverQueryCompletionHandler!, error: NSError!) {
// Here you need to call a function to query the height change
// Send the notification to the user
var notification = UILocalNotification()
notification.alertBody = "Changed height in Health App"
notification.alertAction = "open"
notification.soundName = UILocalNotificationDefaultSoundName
UIApplication.sharedApplication().scheduleLocalNotification(notification)
completionHandler()
}
func authorizeHealthKit(completion: ((success:Bool, error:NSError!) -> Void)!) {
// 1. Set the types you want to read from HK Store
let healthKitTypesToRead = [
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierDateOfBirth),
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBloodType),
HKObjectType.characteristicTypeForIdentifier(HKCharacteristicTypeIdentifierBiologicalSex),
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMass),
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight),
HKObjectType.workoutType()
]
// 2. Set the types you want to write to HK Store
let healthKitTypesToWrite = [
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBodyMassIndex),
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned),
HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning),
HKQuantityType.workoutType()
]
// 3. If the store is not available (for instance, iPad) return an error and don't go on.
if !HKHealthStore.isHealthDataAvailable() {
let error = NSError(domain: "any.domain.com", code: 2, userInfo: [NSLocalizedDescriptionKey:"HealthKit is not available in this Device"])
if( completion != nil ) {
completion(success:false, error:error)
}
return;
}
// 4. Request HealthKit authorization
healthKitStore.requestAuthorizationToShareTypes(Set(healthKitTypesToWrite), readTypes: Set(healthKitTypesToRead)) { (success, error) -> Void in
if( completion != nil ) {
dispatch_async(dispatch_get_main_queue(), self.startObservingHeightChanges)
completion(success:success,error:error)
}
}
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: .Alert | .Badge | .Sound, categories: nil))
self.authorizeHealthKit { (authorized, error) -> Void in
if authorized {
println("HealthKit authorization received.")
}
else {
println("HealthKit authorization denied!")
if error != nil {
println("\(error)")
}
}
}
return true
}
//Rest of the defaults methods of AppDelegate.swift
}
In the above method the HKObserver is activated if the HealthKit authorization is granted by the user and then activate notifications.
I hope this help you.
In iOS 8.1 it does. You need to make sure you recreate your observer queries in your app delegate's application:didFinishLaunchingWithOptions:, though. A bug in 8.0 prevents HealthKit's background notification from working at all.
EDIT:
In your AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//create/get your HKHealthStore instance (called healthStore here)
//get permission to read the data types you need.
//define type, frequency, and predicate (called type, frequency, and predicate here, appropriately)
UIBackgroundTaskIdentifier __block taskID = [application beginBackgroundTaskWithExpirationHandler:^{
if (taskID != UIBackgroundTaskInvalid) {
[application endBackgroundTask:taskID];
taskID = UIBackgroundTaskInvalid;
}
}];
[healthStore enableBackgroundDeliveryForType:type frequency:frequency withCompletion:^(BOOL success, NSError *error) {}];
HKQuery *query = [[HKObserverQuery alloc] initWithSampleType:healthType predicate:predicate updateHandler:
^void(HKObserverQuery *query, HKObserverQueryCompletionHandler completionHandler, NSError *error)
{
//If we don't call the completion handler right away, Apple gets mad. They'll try sending us the same notification here 3 times on a back-off algorithm. The preferred method is we just call the completion handler. Makes me wonder why they even HAVE a completionHandler if we're expected to just call it right away...
if (completionHandler) {
completionHandler();
}
//HANDLE DATA HERE
if (taskID != UIBackgroundTaskInvalid) {
[application endBackgroundTask:taskID];
taskID = UIBackgroundTaskInvalid;
}
}];
[healthStore executeQuery:query];
}