Skip to main content

How to handle Background App Refresh with HealthKit in React Native

How to handle Background App Refresh with HealthKit in React Native

Source: Google...
  • Querying sample data with
  • Background Delivery in HealthKit
  • Background App Refresh
  • Event Emitter for sending event to Javascript
  • Native module cannot be null
  • What is a bridge in React Native?
  • Simulating Background Fetch event
  • Multiple instances of EventEmitter ?
  • Launch due to a background fetch event
  • Headless JS
  • Bridge is not set

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.

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

Then observe with HKObserverQuery

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.

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.

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.

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

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.

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

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

What is a bridge in React Native?

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

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

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

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.

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.

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.

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

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

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.

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

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

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

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

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.

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.

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

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.

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, please mention in comment.

Comments

Popular Posts

Reloading UITableView while Animating Scroll in iOS 11

Reloading UITableView while Animating Scroll Calling  reloadData  on  UITableView  may not be the most efficient way to update your cells, but sometimes it’s easier to ensure the data you are storing is in sync with what your  UITableView  is showing. In iOS 10  reloadData  could be called at any time and it would not affect the scrolling UI of  UITableView . However, in iOS 11 calling  reloadData  while your  UITableView  is animating scrolling causes the  UITableView  to stop its scroll animation and not complete. We noticed this is only true for scroll animations triggered via one of the  UITableView  methods (such as  scrollToRow(at:at:animated:) ) and not for scroll animations caused by user interaction. This can be an issue when server responses trigger a  reloadData  call since they can happen at any moment, possibly when scroll animation is occurring. Example of s...

What are the Alternatives of device UDID in iOS? - iOS7 / iOS 6 / iOS 5 – Get Device Unique Identifier UDID

Get Device Unique Identifier UDID Following code will help you to get the unique-device-identifier known as UDID. No matter what iOS user is using, you can get the UDID of the current iOS device by following code. - ( NSString *)UDID { NSString *uuidString = nil ; // get os version NSUInteger currentOSVersion = [[[[[UIDevice currentDevice ] systemVersion ] componentsSeparatedByString: @" . " ] objectAtIndex: 0 ] integerValue ]; if (currentOSVersion <= 5 ) { if ([[ NSUserDefaults standardUserDefaults ] valueForKey: @" udid " ]) { uuidString = [[ NSUserDefaults standardDefaults ] valueForKey: @" udid " ]; } else { CFUUIDRef uuidRef = CFUUIDCreate ( kCFAllocatorDefault ); uuidString = ( NSString *) CFBridgingRelease ( CFUUIDCreateString ( NULL ,uuidRef)); CFRelease (uuidRef); [[ NSUserDefaults standardUserDefaults ] setObject: uuidString ForKey: @" udid " ]; [[ NSUserDefaults standardUserDefaults ] synchro...

Xcode & Instruments: Measuring Launch time, CPU Usage, Memory Leaks, Energy Impact and Frame Rate

When you’re developing applications for modern mobile devices, it’s vital that you consider the performance footprint that it has on older devices and in less than ideal network conditions. Fortunately Apple provides several powerful tools that enable Engineers to measure, investigate and understand the different performance characteristics of an application running on an iOS device. Recently I spent some time with these tools working to better understand the performance characteristics of an eCommerce application and finding ways that we can optimise the experience for our users. We realised that applications that are increasingly performance intensive, consume excessive amounts of memory, drain battery life and feel uncomfortably slow are less likely to retain users. With the release of iOS 12.0 it’s easier than ever for users to find applications that are consuming the most of their device’s finite amount of resources. Users can now make informed decisions abou...