How to handle Background App Refresh with HealthKit in React Native

Khoa Pham
Khoa Pham
Jan 17, 2019 · 13 min read

Table of Contents

Querying sample data with HKHealthStore

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

let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: endDate,
options: [.strictStartDate, .strictEndDate]
)
let query = HKSampleQuery(
sampleType: HKObjectType.workoutType(),
predicate: predicate,
limit: 0,
sortDescriptors: nil,
resultsHandler: { query, samples, error in
guard error == nil else {
return
}
callback(samples)
})
store.execute(query)

Background Delivery in HealthKit

Next, there is something called background delivery via the enableBackgroundDelivery method

store.enableBackgroundDelivery(
for: HKObjectType.workoutType(),
frequency: .daily,
withCompletion: { succeeded, error in
guard error != nil && succeeded else {
return
}
// Background delivery is enabled
})
let query = HKObserverQuery(
sampleType: HKObjectType.workoutType(),
predicate: nil,
updateHandler: { query, completionHandler, error in
defer {
completionHandler()
}
guard error != nil else {
return
}
// TODO

})
store.execute(query)

Background App Refresh

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

UIBackgroundRefreshStatus status = [UIApplication.sharedApplication backgroundRefreshStatus];
if (status == UIBackgroundRefreshStatusAvailable) {
NSTimeInterval interval = [[Environment sharedInstance] backgroundFetchWakeUpInterval];
[UIApplication.sharedApplication setMinimumBackgroundFetchInterval:interval];
} else {
NSLog(@"Background fetch not available, status: %ld", status);
}
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {  [[BackgroundFetch sharedInstance] queryHealthKitWithCompletion:completionHandler];
}
import Foundation/// Handle backgrounde fetch, query HealthKit data, and use HealthEmitter to send to js
@objc class BackgroundFetch: NSObject {
private static let shared = BackgroundFetch()
@objc let emitter = HealthEmitter()
@objc class func sharedInstance() -> BackgroundFetch {
return shared
}
@objc func queryHealthKit(completion: @escaping (UIBackgroundFetchResult) -> Void) {
guard HealthService.shared.isGranted else {
completion(.noData)
return
}
typealias JSONArray = [[String: Any]] HealthService.shared.readWorkout(callback: { workouts in
let dictionary: [String: Any] = [
"workouts": workouts
]
self.emitter.sendData(dictionary)
self.emitter.completionHandler = completion
})
})
}
}

Event Emitter for sending event to Javascript

Event Emitter is a way to send event from native to Javascript without being invoked directly. You 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 we 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>@property void (^completionHandler)(UIBackgroundFetchResult);/// workouts: NSArray, steps: NSArray
- (void)sendData:(NSDictionary *)json;
@end
#import "HealthEmitter.h"bool hasListeners = NO;@interface HealthEmitter()
@property NSDictionary *json;
@end
@implementation HealthEmitter// Will be called when this module's first listener is added.
- (void)startObserving {
hasListeners = YES;
if (self.json != nil) {
[self 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"];
}
// Not check for hasListeners for now
- (void)sendData:(NSDictionary *)json {
if (hasListeners) {
[self sendEventWithName:@"onSendData" body:json];
} else {
self.json = json;
}
}
RCT_EXPORT_METHOD(finish) {
if (self.completionHandler != nil) {
self.completionHandler(UIBackgroundFetchResultNewData);
}
}
@end

Native module cannot be null

If you get this error, it means that you forget to export the emitter. Go to our HealthEmitter.m and add somewhere inside @implmentation and @end

RCT_EXPORT_MODULE(HealthEmitter);

What is a bridge in React Native?

But when we run the app, we will hit an exception

2019-01-16 13:06:25.177414+0100 MyApp[56426:1094928] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Error when sending event: onSendBooks with body: (
). Bridge is not set. This is probably because you've explicitly synthesized the bridge in HealthEmitter, even though it's inherited from RCTEventEmitter.'
*** First throw call stack:
- (void)sendEventWithName:(NSString *)eventName body:(id)body
{
RCTAssert(_bridge != nil, @"Error when sending event: %@ with body: %@. "
"Bridge is not set. This is probably because you've "
"explicitly synthesized the bridge in %@, even though it's inherited "
"from RCTEventEmitter.", eventName, body, [self class]);
if (RCT_DEBUG && ![[self supportedEvents] containsObject:eventName]) {
RCTLogError(@"`%@` is not a supported event type for %@. Supported events are: `%@`",
eventName, [self class], [[self supportedEvents] componentsJoinedByString:@"`, `"]);
}
if (_listenerCount > 0) {
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter"
method:@"emit"
args:body ? @[eventName, body] : @[eventName]
completion:NULL];
} else {
RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);
}
}
@interface HealthEmitter: RCTEventEmitter <RCTBridgeModule>
/*** Async batched bridge used to communicate with the JavaScript application.*/@interface RCTBridge : NSObject <RCTInvalidating>
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyApp"
initialProperties:nil
launchOptions:launchOptions];
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
moduleProvider:nil
launchOptions:launchOptions];
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}
id<RCTBridgeDelegate> moduleInitialiser = [[classThatImplementsRCTBridgeDelegate alloc] init];RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil];RCTRootView *rootView = [[RCTRootView alloc]
initWithBridge:bridge
moduleName:kModuleName
initialProperties:nil];
[BackgroundFetch sharedInstance].emitter.bridge = rootView.bridge;

Simulating Background Fetch event

Now the exception Bridge 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

// @flowimport { 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: any) => {
console.log('HealthEmitterHandler.onSendData', event)
const { workouts } = event
this.sendToBackEnd(workouts)
}
sendToBackEnd = async (workouts) => {
// send to backend
HealthEmitter.finish()
}
}
const emitterHandler = new HealthEmitterHandler()
export default emitterHandler
// @flowimport 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()
}
render() {
return <AppContainer />
}
}
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 by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
const client = new ApolloClient({
uri: 'https://www.myapp.com/api/',
request: async (operation) => {
//
},
onError: (error) => {
//
},
fetchOptions: {
mode: 'no-cors',
}
})

Multiple instances of EventEmitter ?

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

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

Launch due to a background fetch event

Triggering background fetch event while app is running is not enough. In practice our apps should be wake up from terminated state 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 we hit Run our app is run but there should be no UI .

Running application MyApp ({
initialProps = {
};
rootTag = 11;
})
blob:http://192.168.…-d8e43b30f658:26469 Running application "MyApp" with appParams: {"rootTag":11,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF
@interface HealthEmitter()
@property NSDictionary *json;
@end
// Not check for hasListeners for now
- (void)sendData:(NSDictionary *)json {
if (hasListeners) {
[self sendEventWithName:@"onSendData" body:json];
} else {
self.json = json;
}
}

Headless JS

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 example, to sync fresh data, handle push notifications, or play music. The url for that article has headless-js-android so it is for Android at the moment.

Bridge is not set

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

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

React Native Training

Stories and tutorials for developers interested in React…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store