如何在React Native中使用HealthKit处理后台应用刷新

目录

  • 查询样本数据
  • HealthKit中的后台传递
  • 后台程序刷新
  • 事件发射器,用于将事件发送到Javascript
  • 本机模块不能为空
  • React Native中的桥梁是什么?
  • 模拟后台获取事件
  • EventEmitter的多个实例?
  • 由于后台获取事件而启动
  • 无头JS
  • 未设置网桥

在UI和热重载方面,我很喜欢React Native,但是在React Native中,还有很多简单的事情变得非常困难。 主要是关于处理本机功能和React桥。 这篇文章是关于我的工作,从开始使用iOS的React Native中的Background fetch和HealthKit逐渐了解本机模块。

我正在使用的应用程序具有HealthKit集成,这意味着查询HealthKit数据并积累它们以进行其他研究。 功能之一是定期在后台发送锻炼数据。 可能有一些库可以执行此任务,但是我总是喜欢手动执行,因为React Native包装器应该不会那么重,并且我可以完全控制执行。 本文是关于HealthKit的,但相同的想法也应适用于其他背景需求。

还会有很多React Native源代码拼写,阅读和调试起来有些痛苦,但是最后我们学习了很多事情的实际功能。 在撰写本文时,我使用react-native 0.57.5

HKHealthStore查询样本数据

关于HealthKit,我们可以通过HKHealthStore轻松查询诸如锻炼和步数之类的样本类型。 HKHealthStoreHealthKit管理的所有数据的访问点,我们可以构造HKQueryHKSampleQuery来微调查询。

 让谓词= HKQuery.predicateForSamples( 
withStart:startDate,
结束:endDate,
选项:[。strictStartDate,.strictEndDate]
 让查询= HKSampleQuery( 
sampleType:HKObjectType.workoutType(),
谓词:
限制:0,
sortDescriptors:无,
resultsHandler:{查询,样本,错误
防护错误==无其他{
返回
}
回调(样本)
})
  store.execute(查询) 

HealthKit中的后台传递

接下来,通过enableBackgroundDelivery方法进行后台传递

调用此方法以注册您的应用以进行后台更新。 只要将指定类型的新样本保存到商店,HealthKit就会唤醒您的应用程序。 在指定频率定义的每个时间段内,您的应用最多只能调用一次。

我们可以以一定的频率启用后台传送并设置观察值

  store.enableBackgroundDelivery( 
用于:HKObjectType.workoutType(),
频率:每天
withCompletion:{成功,出现错误
后卫错误!= nil &&成功,否则{
返回
}
  //启用后台传送 
})

然后用HKObserverQuery观察

 让查询= HKObserverQuery( 
sampleType:HKObjectType.workoutType(),
谓词:无,
updateHandler:{查询,completionHandler,错误
推迟{
completeHandler()
}
 保护错误!=无其他{ 
返回
}
  // 去做 

})
  store.execute(查询) 

应用程序还可以通过调用HealthKit商店的enableBackgroundDelivery(for:frequency:withCompletion:)方法进行注册,以在后台接收更新。 此方法注册您的应用以获取后台通知。 只要将指定类型的新样本保存到商店,HealthKit就会唤醒您的应用程序。 每个时间段最多注册一次您的应用,该时间由您在注册时指定的频率定义。

应用启动后,HealthKit会为与新保存的数据匹配的所有观察者查询调用更新处理程序。 如果计划支持后台传送,请在应用程序委托的application(_:didFinishLaunchingWithOptions:)方法中设置所有观察者查询。 通过在application(_:didFinishLaunchingWithOptions:)设置查询,可以确保在HealthKit提供更新之前实例化查询并准备使用它们。

根据Stackoverflow上的一个帖子,后台传递似乎运行顺利。

经过一整天的测试(iOS 9.2),我可以确认HealthKit后台传送在以下所有应用程序状态下均正常工作:

后台 (在后台执行代码),

暂停 (在后台执行但未执行代码),

已终止 (由用户强制杀死或由系统清除)

不幸的是,文档中没有有关每种数据类型的最小频率的信息,但是我对Fitness types经验如下:

有功电能: 每小时

骑车距离: 即时

爬升的航班: 即时

NikeFuel: 立即

步骤: 每小时一次

步行+跑步距离: 每小时

锻炼: 即时

如您所见,何时将新数据保存到HealthKit存储中,我们会收到通知,但仅返回的HKObserverQuery不知道发生了什么变化。 后台传递可能很酷,但是现在我将继续使用传统的后台获取方法来不时查询HealthKit存储。

后台程序刷新

自iOS 7以来,后台应用程序刷新(或简称为后台获取)就已经存在,它可以使您的应用程序在后台定期运行,以便可以更新其内容。 经常更新其内容的应用程序(例如新闻应用程序或社交媒体应用程序)可以使用此功能来确保其内容始终是最新的。 要使用此功能,请首先转到“功能”并启用“后台获取”

然后在AppDelegate.m ,我们与React Native一起使用,所以有AppDelegate.m ,检查backgroundRefreshStatussetMinimumBackgroundFetchInterval (您可能已经猜到了),指定了两次后台获取操作之间必须经过的最短时间。

  UIBackgroundRefreshStatus status = [UIApplication.sharedApplication backgroundRefreshStatus]; 
如果(状态== UIBackgroundRefreshStatusAvailable){
NSTimeInterval interval = [[Environment sharedInstance] backgroundFetchWakeUpInterval];
[UIApplication.sharedApplication setMinimumBackgroundFetchInterval:interval];
}其他{
NSLog(@“背景提取不可用,状态:%ld”,状态);
}

然后实现回调。 当出现下载数据的机会时,系统会调用此方法,使您的应用有机会下载所需的任何数据。 此方法的实现应下载数据,准备该数据以供使用,然后在completionHandler参数中调用该块。

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

我倾向于在S​​wift中使用逻辑,而在Objective C中使用React Native兼容类,因为如果有很多宏,例如RCT_EXPORT_METHODRCT_EXPORT_MODULE ,使用Swift会使我们的生活更加痛苦。

BackgroundFetch是用于处理背景提取的Swift类。 我喜欢将相关的逻辑封装到专用的类中,以便于阅读。 我们可以组织依赖图,但是为了简单起见,我将singleton与共享一起使用。 有时单身人士可以更快地使用React Native。

 进口基金会 
  ///处理backgrounde提取,查询HealthKit数据,并使用HealthEmitter发送给js 
@objc类BackgroundFetch:NSObject {
私人静态让共享= BackgroundFetch()
@objc让发射器= HealthEmitter()
  @objc类func sharedInstance()-> BackgroundFetch { 
返回共享
}
  @objc func queryHealthKit(完成情况:@转义(UIBackgroundFetchResult)->无效){ 
保护HealthService.shared.isGranted其他{
完成(.noData)
返回
}
  typealias JSONArray = [[[String:Any]] 
  HealthService.shared.readWorkout(回调:{在 
让字典:[字符串:任何] = [
“锻炼”:锻炼
]
  self.emitter.sendData(dictionary) 
self.emitter.completionHandler =完成
})
})
}
}

如果您有Array ,则可以将sendData类型转换为JSONArray ,这里有Dictionary因此应将其sendDataemittersendData NSDictionary

HealthEmitter是从RCTEventEmitter继承的Objective C类。 我们可以尝试在Swift中进行制作,但是由于涉及到一些C宏,因此让我们使用Objective C进行快速开发。 最后,我们编写的Objective C类不应包含太多逻辑,它倾向于与React Native类互操作。

事件发射器,用于将事件发送到Javascript

事件发射器是一种将事件从本机发送到Java脚本而无需直接调用的方法。 您可以使用React Native中的Native Modules功能来将方法从Javascript调用到native,并通过回调获取数据。 此事件发射器的工作方式类似于pub子,我们在Javascript中添加了侦听器,本机可以触发事件。 这是我们的HealthEmitter

  #import  
#import
  @interface HealthEmitter:RCTEventEmitter  
  @property void(^ completionHandler)(UIBackgroundFetchResult); 
  ///锻炼:NSArray,步骤:NSArray 
-(void)sendData:(NSDictionary *)json;
  @结束 

之所以存储completionHandler是因为我们希望确保在触发Background App Refresh之前完成工作。

这是我们的HealthEmitter ,负责将数据发送到Javascript。

  #import“ HealthEmitter.h” 
  bool hasListeners = NO; 
  @interface HealthEmitter() 
@property NSDictionary * json;
@结束
  @implementation HealthEmitter 
  //将在添加此模块的第一个侦听器时调用。 
-(void)startObserving {
hasListeners = YES;
如果(self.json!= nil){
[self sendEventWithName:@“ onSendData”正文:self.json];
}
}
  //将在删除该模块的最后一个侦听器时或在dealloc上调用。 
-(void)stopObserving {
hasListeners = NO;
}
  -(NSArray  *)supportedEvents { 
返回@ [@“ onSendData”];
}
  //现在不检查hasListeners 
-(void)sendData:(NSDictionary *)json {
如果(hasListeners){
[self sendEventWithName:@“ onSendData”正文:json];
}其他{
self.json = json;
}
}
  RCT_EXPORT_METHOD(完成){ 
如果(self.completionHandler!= nil){
self.completionHandler(UIBackgroundFetchResultNewData);
}
}
  @结束 

从理论上讲,实现EventEmitter很简单。 我们只需要声明supportedEvents并实现sendData方法。 在这里,我们还实现了finish以手动触发completionHandler

本机模块不能为空

如果收到此错误,则意味着您忘记了导出发射器。 转到我们的HealthEmitter.m并在@implmentation@end内添加一些内容

  RCT_EXPORT_MODULE(健康发射器); 

React Native中的桥梁是什么?

但是,当我们运行该应用程序时,我们将遇到异常

 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:

它抱怨我们的HealthEmitter器缺少桥梁。 对我们来说幸运的是,React Native是开源的,因此我们可以深入研究源代码并查看发生了什么。 转到RCTEventEmitter.m和方法sendEventWithName

  -(void)sendEventWithName:(NSString *)eventName正文:(id)body 
{
RCTAssert(_bridge!= nil,@“发送事件:%@,正文:%@时出错。”
“未设置桥。这可能是因为您已经”
“即使它是继承的,也可以在%@中明确地合成桥”
“来自RCTEventEmitter。”,eventName,body,[self class]);
 如果(RCT_DEBUG &&![[selfsupportedEvents] containsObject:eventName]){ 
RCTLogError(@“ %%`不是%@的受支持事件类型。受支持的事件为:'%@`”,
eventName,[self class],[[selfsupportedEvents] componentsJoinedByString:@“`,`”]));
}
如果(_listenerCount> 0){
[_bridge enqueueJSCall:@“ RCTDeviceEventEmitter”
方法:@“发出”
args:body? @ [eventName,body]:@ [eventName]
完成:NULL];
}其他{
RCTLogWarn(@“发送`%@`,未注册任何侦听器。”,eventName);
}
}

异常警告来自内部RCTAssert 。 我们的HealthEmitter具有此签名。

  @interface HealthEmitter:RCTEventEmitter  

EventEmitter是一个本机模块,它需要符合RCTBridgeModule协议,该协议用于提供注册桥模块所需的接口。

  / ** 
  *用于与JavaScript应用程序通信的异步批处理桥。 
  * / 
  @interface RCTBridge:NSObject  

当我们使用宏RCT_EXPORT_MODULE ,实际上是在RCT_EXPORT_MODULE使用RCTRegisterModule来注册模块,并且据说在我们的类实现中使用此宏可以在加载时自动将模块注册到网桥。

RCTBridge实例加载我们捆绑的js代码,并在iOS的JavascriptCore框架内执行。

如果返回到AppDelegate.m来查看RCTRootView是如何启动的,则可以看到它在引擎盖下创建了一个桥梁。

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

转到RCTRootView.m并查看桥的构造方式。 如您所见,当应用程序仅使用1 RCTRootView时,将使用此方法。 例如,如果我们计划使用更多的RCTRootView ,而当我们想在现有的本机应用程序中包含一些React Native视图时,则需要为每个视图创建更多的RCTBridge

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

如果您希望通过自己的网桥进行依赖注入,那么创建网桥就不难了。

桥会自动初始化所​​有已注册的RCTBridgeModules

  id  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; 

模拟后台获取事件

现在Bridge is not set的异常Bridge is not set消失了。 让我们通过running Xcode -> Debug -> Simulate Background Fetch来测试后台应用程序在running时是否刷新

在测试之前,我们需要在Javascript中使用本机发射器。 创建一个名为EmitterHandler.js的类,然后调用EmitterHandler.subscribe订阅EventEmitter事件。

  // @流 
 从'react-native'导入{NativeEventEmitter,NativeModules} 
  const HealthEmitter = NativeModules.HealthEmitter 
const eventEmitter =新的NativeEventEmitter(HealthEmitter)
 类EmitterHandler { 
订阅:任何
  subscription(){ 
  this.subscription = eventEmitter.addListener('onSendData',this.onSendData) 
console.log('subscribeEmitter',this.subscription)
}
  unsubscribe(){ 
this.subscription.remove()
}
  onSendData =(event:any)=> { 
console.log('HealthEmitterHandler.onSendData',事件)
const {锻炼} =事件
this.sendToBackEnd(锻炼)
}
  sendToBackEnd =异步(锻炼)=> { 
//发送到后端
HealthEmitter.finish()
}
}
  const collectorHandler =新的HealthEmitterHandler() 
导出默认的generatorHandler

请注意, eventEmitter的类型为NativeEventEmitter ,用于调用addListener ,但是我们需要在HealthEmitter上调用finishHealthEmitter通过RCT_EXPORT_METHOD(finish)公开。

例如,我们可以在App.js观察到

  // @流 
 从'react'导入React 
从“反应导航”导入{createAppContainer}
从'./src/screens/root/RootNavigator'导入makeRootNavigator
从'./src/EmitterHandler'导入EmitterHandler
  const RootNavigator = makeRootNavigator({}) 
const AppContainer = createAppContainer(RootNavigator)
 输入Props = {} 
 导出默认类App扩展React.Component  { 
componentDidMount(){
EmitterHandler.subscribe()
}
  render(){ 
返回
}
}

我们可以在模拟器中测试模拟后台获取事件。 每当触发事件时,我们都可以看到调用了application:performFetchWithCompletionHandler方法,然后在Javascript端触发了onSendData

我们可能会遇到Access-Control-Allow-Origin问题。 同源策略是一种重要的安全机制,它限制了从一个来源加载的文档或脚本如何与另一个来源的资源进行交互。 当一个域(例如http://foo.com/ )从另一个域(例如http://bar.com/ )请求资源时,就会发生跨域请求。

 通过CORS策略阻止从来源“ http:// localhost:8081”访问“ http://192.168.0.13:8081/index.delta?platform=ios&dev=true&minify=false”的访存:无“访问控制” -Allow-Origin'标头出现在请求的资源上。 如果不透明的响应满足您的需求,请将请求的模式设置为“ no-cors”,以在禁用CORS的情况下获取资源。 

如果像我一样使用ApolloClient,则可以自定义fetchOptions

  const client = new ApolloClient({ 
uri:“ https://www.myapp.com/api/”,
请求:异步(操作)=> {
//
},
onError:(错误)=> {
//
},
fetchOptions:{
模式:“ no-cors”,
}
})

EventEmitter的多个实例?

回到我们的HealthEmitter ,在startObservingsendData设置断点,令人惊讶的是,我们看到了不同的实例。 您可以通过查看Xcode调试器中的地址号来进行检查。

我不知道,这就是为什么我宣布bool hasListeners = NO; 作为全局变量,而不是在HealthEmitter内部。 此解决方法现在就足够了。

关于starObservingstopObserving

分别在添加第一个观察者和删除最后一个观察者时(或在调用dealloc时)分别调用这些方法。 这些应该在您的子类中重写,以便开始/停止发送事件。

当有侦听器时,观察工作正常,您可以通过将断点放入RCTEventEmitter.m来验证这RCTEventEmitter.m

  RCT_EXPORT_METHOD(addListener:(NSString *)eventName) 
{
如果(RCT_DEBUG &&![[selfsupportedEvents] containsObject:eventName]){
RCTLogError(@“ %%`不是%@的受支持事件类型。受支持的事件为:'%@`”,
eventName,[self class],[[selfsupportedEvents] componentsJoinedByString:@“`,`”]));
}
_listenerCount ++;
如果(_listenerCount == 1){
[self startObserving];
}
}

由于后台获取事件而启动

仅在应用程序运行时触发后台获取事件是不够的。 实际上,由于后台获取事件,我们的应用应从终止状态唤醒。 为了模拟这一点,我们需要在方案中调整“ Options 。 现在检查Launch due to a background fetch event ,当我们点击Run我们的应用程序已运行,但应该没有UI。

这项测试在模拟器中似乎不可行,因此最好在设备中进行测试。

AppDelegate.m ,将按顺序调用application:didFinishLaunchingWithOptions:application:performFetchWithCompletionHandler

但是这是奇怪的事情发生的地方😱我不知道它是否与React Native缓存有关,或者React控制台显示不正确,但有时它不显示用于正常运行的正在Running application消息。

 正在运行应用程序MyApp({ 
initialProps = {
};
rootTag = 11;
})
blob:http://192.168.…-d8e43b30f658:26469运行带有appParams的应用程序“ MyApp”:{“ rootTag”:11,“ initialProps”:{}}。 __DEV__ === true,开发级别警告为ON,性能优化为OFF

有一阵子我想知道RCTBundleURLProvider是否不能在由于后台获取而醒来的情况下工作,因为在这种情况下还没有UI,并且UIApplication状态不处于活动状态。

有一件事发生的频率更高,而不是所有时间都在发生,但是确实发生了。 有时,在sendData中触发HealthEmitter之后,会调用sendData中的HealthEmitter 。 这会导致Sending onSendData with no listeners registered发出警告,因为当前没有侦听器。

  @interface HealthEmitter() 
@property NSDictionary * json;
@结束

一种快速的解决方法是将数据存储在HealthEmitter ,然后检查是否已将数据和hasListeners设置为true。

  //现在不检查hasListeners 
-(void)sendData:(NSDictionary *)json {
如果(hasListeners){
[self sendEventWithName:@“ onSendData”正文:json];
}其他{
self.json = json;
}
}

无头JS

还有一种叫做Headless的东西,它是一种在您的应用程序在后台运行时使用JavaScript运行任务的方法。 例如,它可用于同步新数据,处理推送通知或播放音乐。 该文章的网址具有headless-js-android因此目前适用于Android。

未设置网桥

我们确实接近一个可行的解决方案,但是在运行时,又会出现旧的网桥错误。 这有点不同

未设置网桥。 这可能是因为您已经在HealthEmitter中明确地合成了该桥,即使它是从RCTEventEmitter继承而来的。

要解决此问题并避免出现多个HealthEmitter实例问题,请使用singleton确保周围只有1个实例。

  @implementation HealthEmitter 
  +(id)allocWithZone:(NSZone *)zone { 
静态HealthEmitter * sharedInstance = nil;
静态dispatch_once_t OnceToken;
dispatch_once(&onceToken,^ {
sharedInstance = [超级allocWithZone:zone];
});
返回sharedInstance;
}
  @结束 

区域就像分配对象的内存区域。 该类方法用于返回接收类的新实例。

现在,所有内容都已编译,并且我们的定期后台获取应该可以按预期进行。 感谢您关注这么长的帖子和旅程,希望您能学到有用的东西。 关键是要仔细测试并愿意深入研究代码以发现问题。