How to handle Background App Refresh with HealthKit in React Native

Khoa Pham
Khoa Pham
Jan 17 · 13 min read

Table of Contents

I enjoy React Native when it comes to UI and hot reloading, but there are also many simple things that becomes really hard in React Native. Mostly it is about dealing with native capability and React bridge. This article is about my journey 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 data and accumulate them for other researches. One of the feature is to periodically send workout data in the background. There may be some libraries for this task, but I always prefer doing manually as the React Native wrapper should not be that heavy, and I have complete control over the execution. This article is about HealthKit, but the same idea should work for other background needs too.

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

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

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 called at most once per time period defined by the specified frequency.

We can enable background delivery with a certain frequency and set up observations

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

Then observe with HKObserverQuery

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

})
store.execute(query)

Apps can also register to receive updates while in the background by calling the HealthKit store’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 time period defined by the frequency you specified when registering.

As soon as your app launches, HealthKit calls the update handler for any observer queries that match the newly saved data. If you plan on supporting background delivery, set up all your observer queries in your app delegate’s application(_:didFinishLaunchingWithOptions:) method. By setting up the queries in application(_: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 day of testing (iOS 9.2) 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 but not executing code),

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

Unfortunately, there is no info in documentation about minimum frequencies per data types, 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.

As you can see, we are notified when new data is saved into HealthKit store, but we don’t know what has changed with just the returned HKObserverQuery . Background delivery may be cool, but for now I will go with traditional background fetch approach to query HealthKit store every now and then.

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

Then in AppDelegate.m , yay we working with React Native so there is AppDelegate.m , check backgroundRefreshStatus and setMinimumBackgroundFetchInterval , which you may already guess, specifies the minimum amount of time that must elapse between background fetch operations.

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);
}

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

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

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

BackgroundFetch is a Swift class used to handle Background fetch. I like to encapsulate related logic into dedicated class so it is more readable. We can 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 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
})
})
}
}

If you have Array, you can type cast to JSONArray , here I have Dictionary so it should be casted into NSDictionary in sendData in emitter.

HealthEmitter is an Objective C class that inherits from RCTEventEmitter . We can try making it in Swift, but since there are some C macros involved, let’s going with Objective C for quick development. And in the end, the Objective C class we write should not contain much logic, it is tended to be interopped with React Native classes.

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

The reason we store completionHandler is we want to ensure our work finishes before trigger Background App Refresh.

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

#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

In theory, implementing EventEmitter is simple. We just need to declare supportedEvents and implement the sendData method. Here we also implement finish to manually trigger completionHandler

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:

It complains that our HealthEmitter is missing a bridge. Luckily for us, React Native is open source so we can dig into the source and see what’s happening. Go to RCTEventEmitter.m and the method sendEventWithName

- (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);
}
}

The exception warning is from the insideRCTAssert . 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 application.*/@interface RCTBridge : NSObject <RCTInvalidating>

And when we use 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 class 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.m to see how RCTRootView gets initiated, we can see that it creates a bridge under the hood.

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

Go to RCTRootView.m and see how the bridge is constructed. As you can see, this method is used when the app uses only 1 RCTRootView . If we plan to use more RCTRootView , for example, when we want to have some React Native views inside our existing native app, then 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:nil
launchOptions:launchOptions];
return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
}

If you wish to have dependency injection with your own bridge, it’s not that hard to create one.

The bridge initializes any registered RCTBridgeModules automatically

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

Correct me if I’m wrong, here we ‘re going to reuse that single bridge for our emitter too

[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

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

// @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

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

We can observe, for example, in App.js

// @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 />
}
}

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

We may hit Access-Control-Allow-Origin issue. The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. A cross-origin request occurs when one domain (for example http://foo.com/) requests a resource 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 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.

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

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.

I have 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 will be called when the first observer is added and when the last observer is removed (or when dealloc is called), respectively. These should be overridden in your subclass in order to start/stop sending events.

Observation works when there is listeners, you can verify this by putting breakpoint into RCTEventEmitter.m

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 .

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 be called in order.

But here is where the weird things happen 😱 I don’t know if it relates to React Native cache or that the React console not display correct, but sometimes it does not 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 application "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 can’t work in event like waking up due to background fetch, because in this case there is no UI yet, and that UIApplication state is not active.

There is one thing that happens more frequently, not all the times, but it 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 as there is no listeners at the moment.

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

A quick workaround is to store data inside HealthEmitter , then check that we have both data and hasListeners set to true.

// 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

Bridge is not set. This is probably because you’ve explicitly synthesized the bridge in HealthEmitter, even though it’s inherited from RCTEventEmitter.’

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

@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

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

Now all are compiled, and our periodic background fetch should work as expected. Thanks for following such a long post and journey, hope you learn something useful. The key is to carefully test and be willing to dig into code to find the problems.


If you like this post, consider visiting my other articles and apps 🔥

React Native Training

Stories and tutorials for developers interested in React Native

Khoa Pham

Written by

Khoa Pham

My apps https://onmyway133.github.io/

React Native Training

Stories and tutorials for developers interested in React Native

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade