Healthkit How to Listen for Read Changes

How to handle Background App Refresh with HealthKit in React Native

Source: Vietnam Travel Guide
  • Querying sample data with
  • Background Commitment in HealthKit
  • Background App Refresh
  • Event Emitter for sending event to Javascript
  • Native module cannot exist goose egg
  • What is a bridge in React Native?
  • Simulating Background Fetch event
  • Multiple instances of EventEmitter ?
  • Launch due to a background fetch event
  • Headless JS
  • Span is non set

I relish React Native when it comes to UI and hot reloading, just in that location are also many uncomplicated things that becomes actually hard in React Native. Mostly it is about dealing with native adequacy and React bridge. This article is most my journeying from pulling hairs into gradually understanding native module with Background fetch and HealthKit in React Native for iOS.

The app that I'm working with has HealthKit integration, that means querying for HealthKit information and accumulate them for other researches. One of the feature is to periodically ship workout information in the groundwork. In that location may be some libraries for this task, but I e'er prefer doing manually as the React Native wrapper should non be that heavy, and I have complete command over the execution. This article is virtually HealthKit, but the same idea should work for other background needs too.

There will besides be a lot of React Native source code spelunking, it is a bit painful to read and debug, but in the end nosotros learn a lot how things really function. As the time of writing, I'm using react-native 0.57.5

A bit well-nigh HealthKit, there are sample types such as workout and step count that we can easily query with HKHealthStore . HKHealthStore is the access betoken for all data managed by HealthKit, and nosotros can construct HKQuery and HKSampleQuery to fine tune the queries.

          allow predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictStartDate, .strictEndDate]
)
allow query = HKSampleQuery(
sampleType: HKObjectType.workoutType(),
predicate: predicate,
limit: 0,
sortDescriptors: naught,
resultsHandler: { query, samples, error in
baby-sit error == nil else {
render
}
callback(samples)
})
shop.execute(query)

Side by side, there is something called groundwork commitment via the enableBackgroundDelivery method

Call this method to register your app for background updates. HealthKit wakes your app whenever new samples of the specified type are saved to the store. Your app is chosen at most in one case per time period defined past the specified frequency.

We tin enable background commitment with a certain frequency and ready observations

          shop.enableBackgroundDelivery(
for: HKObjectType.workoutType(),
frequency: .daily,
withCompletion: { succeeded, error in
guard fault != nil && succeeded else {
return
}
// Background delivery is enabled
})

Then discover with HKObserverQuery

          let query = HKObserverQuery(
sampleType: HKObjectType.workoutType(),
predicate: nil,
updateHandler: { query, completionHandler, error in
defer {
completionHandler()
}
guard mistake != cypher else {
return
}
// TODO

})

shop.execute(query)

Apps tin can too register to receive updates while in the background by calling the HealthKit shop's enableBackgroundDelivery(for:frequency:withCompletion:) method. This method registers your app for background notifications. HealthKit wakes your app whenever new samples of the specified type are saved to the store. Your app is called at most once per fourth dimension period divers by the frequency yous specified when registering.

As soon equally your app launches, HealthKit calls the update handler for any observer queries that match the newly saved data. If y'all plan on supporting background delivery, ready all your observer queries in your app delegate's awarding(_:didFinishLaunchingWithOptions:) method. By setting up the queries in awarding(_:didFinishLaunchingWithOptions:), you ensure that the queries are instantiated and ready to use before HealthKit delivers the updates.

According to a thread on Stackoverflow, background delivery seems to work smoothly.

After a full twenty-four hour period of testing (iOS 9.2) I can ostend that HealthKit groundwork delivery DOES WORK in all of the following application states:

background (in background and executing lawmaking),

suspended (in background but not executing code),

terminated (force-killed past the user or purged by the system)

Unfortunately, there is no info in documentation well-nigh minimum frequencies per information types, but my experience with Fitness types was as follows:

Agile Energy: hourly,

Cycling Altitude: immediate,

Flights Climbed: immediate,

NikeFuel: immediate,

Steps: hourly,

Walking + Running Altitude: hourly,

Workouts: immediate.

As y'all tin can come across, we are notified when new data is saved into HealthKit shop, but we don't know what has changed with just the returned HKObserverQuery . Groundwork delivery may be cool, but for at present I volition go with traditional background fetch approach to query HealthKit shop every now then.

Background App Refresh, or simply Background fetch, has been around since iOS 7, which lets your app run periodically in the background so that information technology can update its content. Apps that update their content frequently, such as news apps or social media apps, can utilize this feature to ensure that their content is always up to date. To piece of work with this, first go to Capabilities and enable Background fetch

And so in AppDelegate.m , yay we working with React Native so in that location is AppDelegate.1000 , check backgroundRefreshStatus and setMinimumBackgroundFetchInterval , which y'all may already approximate, specifies the minimum amount of fourth dimension that must elapse between background fetch operations.

          UIBackgroundRefreshStatus status = [UIApplication.sharedApplication backgroundRefreshStatus];
if (status == UIBackgroundRefreshStatusAvailable) {
NSTimeInterval interval = [[Surround sharedInstance] backgroundFetchWakeUpInterval];
[UIApplication.sharedApplication setMinimumBackgroundFetchInterval:interval];
} else {
NSLog(@"Background fetch not available, condition: %ld", status);
}

Then implement the callback. When an opportunity arises to download data, the system calls this method to give your app a take a chance to download any data information technology needs. Your implementation of this method should download the information, set up that data for use, and telephone call the block in the completionHandler parameter.

          - (void)application:(UIApplication *)awarding performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {                      [[BackgroundFetch sharedInstance] queryHealthKitWithCompletion:completionHandler];
}

I tend to have logic in Swift, and React Native uniform classes in Objective C, considering there are many macros like RCT_EXPORT_METHOD and RCT_EXPORT_MODULE that would make our lives more miserable if nosotros go with Swift.

BackgroundFetch is a Swift class used to handle Groundwork fetch. I like to encapsulate related logic into dedicated class and so it is more than readable. We tin organise dependencies graph, but I use singleton with shared for simplicity. And sometimes singleton makes working with React Native faster.

          import Foundation          /// Handle backgrounde fetch, query HealthKit data, and employ HealthEmitter to transport to js
@objc course BackgroundFetch: NSObject {
private static allow shared = BackgroundFetch()
@objc let emitter = HealthEmitter()
@objc grade func sharedInstance() -> BackgroundFetch {
return shared
}
@objc func queryHealthKit(completion: @escaping (UIBackgroundFetchResult) -> Void) {
baby-sit HealthService.shared.isGranted else {
completion(.noData)
return
}
typealias JSONArray = [[String: Whatever]] HealthService.shared.readWorkout(callback: { workouts in
let dictionary: [String: Whatsoever] = [
"workouts": workouts
]
self.emitter.sendData(dictionary)
self.emitter.completionHandler = completion
})
})
}
}

If you have Array, y'all can type bandage to JSONArray , here I accept Dictionary and then it should exist casted into NSDictionary in sendData in emitter.

HealthEmitter is an Objective C class that inherits from RCTEventEmitter . Nosotros can attempt making it in Swift, merely since there are some C macros involved, let'southward going with Objective C for quick development. And in the terminate, the Objective C grade we write should not contain much logic, it is tended to be interopped with React Native classes.

Consequence Emitter is a way to send event from native to Javascript without being invoked directly. Yous may used Native Modules feature in React Native to call methods from Javascript to native, and get data via a callback. This Event Emitter works like a pub sub, that nosotros add listener in Javascript, and native can trigger events. Here is our HealthEmitter

          #import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface HealthEmitter: RCTEventEmitter <RCTBridgeModule> @belongings void (^completionHandler)(UIBackgroundFetchResult); /// workouts: NSArray, steps: NSArray
- (void)sendData:(NSDictionary *)json;
@end

The reason we shop completionHandler is nosotros want to ensure our piece of work finishes before trigger Groundwork App Refresh.

And here is our HealthEmitter where it is responsible for sending information to Javascript.

          #import "HealthEmitter.h"          bool hasListeners = NO;          @interface HealthEmitter()
@property NSDictionary *json;
@cease
@implementation HealthEmitter // Will be called when this module'southward starting time listener is added.
- (void)startObserving {
hasListeners = Yeah;
if (self.json != null) {
[cocky sendEventWithName:@"onSendData" body:self.json];
}
}
// Will be called when this module's last listener is removed, or on dealloc.
- (void)stopObserving {
hasListeners = NO;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"onSendData"];
}
// Non bank check for hasListeners for now
- (void)sendData:(NSDictionary *)json {
if (hasListeners) {
[cocky sendEventWithName:@"onSendData" body:json];
} else {
self.json = json;
}
}
RCT_EXPORT_METHOD(finish) {
if (self.completionHandler != nix) {
self.completionHandler(UIBackgroundFetchResultNewData);
}
}
@terminate

In theory, implementing EventEmitter is simple. Nosotros just demand to declare supportedEvents and implement the sendData method. Hither we also implement end to manually trigger completionHandler

If you get this error, it means that you forget to consign the emitter. Go to our HealthEmitter.chiliad and add somewhere inside @implmentation and @terminate

          RCT_EXPORT_MODULE(HealthEmitter);        

But when nosotros run the app, nosotros will hitting an exception

          2019-01-16 13:06:25.177414+0100 MyApp[56426:1094928] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error when sending upshot: onSendBooks with body: (
). Bridge is non set. This is probably because y'all've explicitly synthesized the bridge in HealthEmitter, even though it's inherited from RCTEventEmitter.'
*** Get-go throw call stack:

Information technology complains that our HealthEmitter is missing a bridge. Luckily for usa, React Native is open source so we can dig into the source and see what'due south happening. Go to RCTEventEmitter.thou and the method sendEventWithName

          - (void)sendEventWithName:(NSString *)eventName body:(id)body
{
RCTAssert(_bridge != nil, @"Fault when sending consequence: %@ with torso: %@. "
"Bridge is not ready. This is probably because yous've "
"explicitly synthesized the bridge in %@, even though it'south inherited "
"from RCTEventEmitter.", eventName, body, [self class]);
if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) {
RCTLogError(@"`%@` is not a supported event blazon for %@. Supported events are: `%@`",
eventName, [self grade], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
}
if (_listenerCount > 0) {
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
method:@"emit"
args:body ? @[eventName, trunk] : @[eventName]
completion:NULL];
} else {
RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);
}
}

The exception warning is from the withinRCTAssert . Our HealthEmitter has this signature.

          @interface HealthEmitter: RCTEventEmitter <RCTBridgeModule>        

EventEmitter is a native module, and it needs to conform to RCTBridgeModule protocol, which is used to provides the interface needed to register a bridge module.

          /**          * Async batched bridge used to communicate with the JavaScript awarding.          */          @interface RCTBridge : NSObject <RCTInvalidating>        

And when nosotros employ the macro RCT_EXPORT_MODULE , this actually uses RCTRegisterModule under the hood to register module, and it is said that with this macro in our grade implementation to automatically register our module with the bridge when it loads.

RCTBridge instance loads our bundled js code and execute it inside JavascriptCore framework in iOS.

If we go back to our AppDelegate.k to see how RCTRootView gets initiated, nosotros can run across that it creates a bridge under the hood.

          jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyApp"
initialProperties:goose egg
launchOptions:launchOptions];

Go to RCTRootView.thousand and see how the span is constructed. Every bit you can encounter, this method is used when the app uses only ane RCTRootView . If we program to apply more RCTRootView , for instance, when we want to have some React Native views inside our existing native app, so we need to create more RCTBridge for each view.

          - (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
moduleProvider:nothing
launchOptions:launchOptions];
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}

If you wish to take dependency injection with your own span, information technology'south not that hard to create i.

The span initializes any registered RCTBridgeModules automatically

          id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];          RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:null];          RCTRootView *rootView = [[RCTRootView alloc]
initWithBridge:bridge
moduleName:kModuleName
initialProperties:nada];

Correct me if I'm wrong, hither nosotros 're going to reuse that single span for our emitter too

          [BackgroundFetch sharedInstance].emitter.bridge = rootView.span;        

At present the exception Span is not set has gone. Let's test our Background App Refresh when the app is running , by going to Xcode -> Debug -> Simulate Background Fetch

Before testing, we need to consume our native emitter inside Javascript. Create a class called EmitterHandler.js , then phone call EmitterHandler.subscribe to subscribe to EventEmitter events.

          // @flow          import { NativeEventEmitter, NativeModules } from 'react-native'          const HealthEmitter = NativeModules.HealthEmitter
const eventEmitter = new NativeEventEmitter(HealthEmitter)
class EmitterHandler {
subscription: any
subscribe() { this.subscription = eventEmitter.addListener('onSendData', this.onSendData)
console.log('subscribeEmitter', this.subscription)
}
unsubscribe() {
this.subscription.remove()
}
onSendData = (event: whatever) => {
console.log('HealthEmitterHandler.onSendData', effect)
const { workouts } = event
this.sendToBackEnd(workouts)
}
sendToBackEnd = async (workouts) => {
// send to backend
HealthEmitter.cease()
}
}
const emitterHandler = new HealthEmitterHandler()
export default emitterHandler

Note that eventEmitter is of type NativeEventEmitter and is used to call addListener , simply nosotros need to call finish on our HealthEmitter , which we exposed with RCT_EXPORT_METHOD(cease) .

We tin notice, for example, in App.js

          // @period          import React from 'react'
import { createAppContainer } from 'react-navigation'
import makeRootNavigator from './src/screens/root/RootNavigator'
import EmitterHandler from './src/EmitterHandler'
const RootNavigator = makeRootNavigator({})
const AppContainer = createAppContainer(RootNavigator)
type Props = {} export default class App extends React.Component<Props> {
componentDidMount() {
EmitterHandler.subscribe()
}
return() {
return <AppContainer />
}
}

We can test simulating background fetch issue in Simulator. Whenever an event is triggered, we can see that the method awarding:performFetchWithCompletionHandler is called, then onSendData is triggered in Javascript side.

We may hit Admission-Command-Permit-Origin upshot. The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can collaborate with a resources from another origin. A cross-origin asking occurs when one domain (for example http://foo.com/) requests a resources from a separate domain (for example http://bar.com/).

          Access to fetch at 'http://192.168.0.13:8081/index.delta?platform=ios&dev=true&minify=false' from origin 'http://localhost:8081' has been blocked past CORS policy: No 'Admission-Command-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, gear up the request's style to 'no-cors' to fetch the resources with CORS disabled.        

If you use ApolloClient like me, and then you lot can customize fetchOptions

          const customer = new ApolloClient({
uri: 'https://world wide web.myapp.com/api/',
asking: async (operation) => {
//
},
onError: (fault) => {
//
},
fetchOptions: {
mode: 'no-cors',
}
})

Go back to our HealthEmitter , put breakpoints in startObserving and sendData , and surprisingly we see different instances. You can check that past looking at the accost number in Xcode debugger.

I take no idea, that's why I declare bool hasListeners = NO; as a global variable, not inside HealthEmitter . This workaround should suffice for now.

A bit about starObserving and stopObserving

These methods volition exist called when the first observer is added and when the concluding observer is removed (or when dealloc is called), respectively. These should exist overridden in your subclass in order to get-go/stop sending events.

Observation works when there is listeners, you tin can verify this past putting breakpoint into RCTEventEmitter.thou

          RCT_EXPORT_METHOD(addListener:(NSString *)eventName)
{
if (RCT_DEBUG && ![[cocky supportedEvents] containsObject:eventName]) {
RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`",
eventName, [self grade], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
}
_listenerCount++;
if (_listenerCount == ane) {
[self startObserving];
}
}

Triggering background fetch event while app is running is not enough. In practice our apps should be wake upwardly from terminated land due to background fetch event. To simulate that, we need to tweak Options in our scheme. Check Launch due to a background fetch event , now when nosotros hit Run our app is run just there should be no UI .

This testing does not seem to be feasible in simulator, so it's best to test in device.

In AppDelegate.m , application:didFinishLaunchingWithOptions: and application:performFetchWithCompletionHandler will exist chosen in order.

Simply hither is where the weird things happen 😱 I don't know if it relates to React Native cache or that the React panel non display right, simply sometimes it does non display Running application message that it used to show for normal run.

          Running application MyApp ({
initialProps = {
};
rootTag = 11;
})
blob:http://192.168.…-d8e43b30f658:26469 Running awarding "MyApp" with appParams: {"rootTag":11,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF

For a while I was wondering if RCTBundleURLProvider tin't work in event like waking up due to background fetch, considering in this case there is no UI yet, and that UIApplication state is non agile.

There is one thing that happens more frequently, not all the times, only information technology does happen. That is sometimes the subscribe method in EmitterHandler is called after sendData is triggered in HealthEmitter . This cause Sending onSendData with no listeners registered warning every bit there is no listeners at the moment.

          @interface HealthEmitter()
@property NSDictionary *json;
@end

A quick workaround is to store data inside HealthEmitter , and so cheque that we have both data and hasListeners prepare to true.

          // Not check for hasListeners for at present
- (void)sendData:(NSDictionary *)json {
if (hasListeners) {
[self sendEventWithName:@"onSendData" body:json];
} else {
cocky.json = json;
}
}

There is also something called Headless, which is a way to run tasks in JavaScript while your app is in the background. It can be used, for case, to sync fresh data, handle push button notifications, or play music. The url for that article has headless-js-android so it is for Android at the moment.

We are really close to a working solution, just when running there is the old bridge error over again. This is a bit unlike

Bridge is not gear up. This is probably because y'all've explicitly synthesized the bridge in HealthEmitter, even though it'due south inherited from RCTEventEmitter.'

To solve this and to avoid the multiple HealthEmitter instances trouble, allow use singleton to ensure there is only 1 instance around.

          @implementation HealthEmitter          + (id)allocWithZone:(NSZone *)zone {
static HealthEmitter *sharedInstance = nada;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [super allocWithZone:zone];
});
return sharedInstance;
}
@end

A zone is like a region of memory where objects are allocated. This class method is used to returns a new case of the receiving form.

At present all are compiled, and our periodic groundwork fetch should work as expected. Thanks for following such a long post and journey, hope you learn something useful. The fundamental is to carefully exam and be willing to dig into code to discover the problems.

breweringthe.blogspot.com

Source: https://medium.com/react-native-training/how-to-handle-background-app-refresh-with-healthkit-in-react-native-3a32704461fe

0 Response to "Healthkit How to Listen for Read Changes"

Отправить комментарий

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel