iOS获取健康步数从加速计到healthkit
又改需求了,所以又换了全新的统计步数的方法,整理一下吧。
在iPhone5s以前机型因为没有陀螺仪的存在,所以需要用加速度传感器来采集加速度值信息,然后根据震动幅度让其加入踩点数组并过滤,获取自己需要的步数数据。
直接上代码吧:
首先需要一个步数的model如下:
#import <Foundation/Foundation.h> @interface VHSSteps : NSObject //步数模型 @property(nonatomic,strong) NSDate *date; @property(nonatomic,assign) int record_no; @property(nonatomic, strong) NSString *record_time; @property(nonatomic,assign) int step; //g是一个震动幅度的系数,通过一定的判断条件来判断是否计做一步 @property(nonatomic,assign) double g; @end
然后是如何获取步数,首先判断传感器是否可用
//加速度传感器 self.motionManager = [[CMMotionManager alloc] init]; // 检查传感器到底在设备上是否可用 if (!self.motionManager.accelerometerAvailable) { return; } else { // 更新频率是100Hz //以pull方式获取数据 self.motionManager.accelerometerUpdateInterval = 1.0/40; }
可用的话开始实现统计步数的算法
@try { //如果不支持陀螺仪,需要用加速传感器来采集数据 if (!self.motionManager.isAccelerometerActive) {// isAccelerometerAvailable方法用来查看加速度器的状态:是否Active(启动)。 // 加速度传感器采集的原始数组 if (arrAll == nil) { arrAll = [[NSMutableArray alloc] init]; } else { [arrAll removeAllObjects]; } /* 1.push方式 这种方式,是实时获取到Accelerometer的数据,并且用相应的队列来显示。即主动获取加速计的数据。 */ NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [self.motionManager startAccelerometerUpdatesToQueue:queue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error){ if (!self.motionManager.isAccelerometerActive) { return; } //三个方向加速度值 double x = accelerometerData.acceleration.x; double y = accelerometerData.acceleration.y; double z = accelerometerData.acceleration.z; //g是一个double值 ,根据它的大小来判断是否计为1步. double g = sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)) - 1; //将信息保存在步数模型中 VHSSteps *stepsAll = [[VHSSteps alloc] init]; stepsAll.date = [NSDate date]; //日期 NSDateFormatter *df = [[NSDateFormatter alloc] init] ; df.dateFormat = @"yyyy-MM-dd HH:mm:ss"; NSString *strYmd = [df stringFromDate:stepsAll.date]; df = nil; stepsAll.record_time =strYmd; stepsAll.g = g; // 加速度传感器采集的原始数组 [arrAll addObject:stepsAll]; // 每采集10条,大约1.2秒的数据时,进行分析 if (arrAll.count == 10) { // 步数缓存数组 NSMutableArray *arrBuffer = [[NSMutableArray alloc] init]; arrBuffer = [arrAll copy]; [arrAll removeAllObjects]; // 踩点数组 NSMutableArray *arrCaiDian = [[NSMutableArray alloc] init]; //遍历步数缓存数组 for (int i = 1; i < arrBuffer.count - 2; i++) { //如果数组个数大于3,继续,否则跳出循环,用连续的三个点,要判断其振幅是否一样,如果一样,然并卵 if (![arrBuffer objectAtIndex:i-1] || ![arrBuffer objectAtIndex:i] || ![arrBuffer objectAtIndex:i+1]) { continue; } VHSSteps *bufferPrevious = (VHSSteps *)[arrBuffer objectAtIndex:i-1]; VHSSteps *bufferCurrent = (VHSSteps *)[arrBuffer objectAtIndex:i]; VHSSteps *bufferNext = (VHSSteps *)[arrBuffer objectAtIndex:i+1]; //控制震动幅度,,,,,,根据震动幅度让其加入踩点数组, if (bufferCurrent.g < -0.12 && bufferCurrent.g < bufferPrevious.g && bufferCurrent.g < bufferNext.g) { [arrCaiDian addObject:bufferCurrent]; } } //如果没有步数数组,初始化 if (nil == self.arrSteps) { self.arrSteps = [[NSMutableArray alloc] init]; self.arrStepsSave = [[NSMutableArray alloc] init]; } // 踩点过滤 for (int j = 0; j < arrCaiDian.count; j++) { VHSSteps *caidianCurrent = (VHSSteps *)[arrCaiDian objectAtIndex:j]; //如果之前的步数为0,则重新开始记录 if (self.arrSteps.count == 0) { //上次记录的时间 lastDate = caidianCurrent.date; // 重新开始时,纪录No初始化 record_no = 1; record_no_save = 1; // 运动识别号 NSTimeInterval interval = [caidianCurrent.date timeIntervalSince1970]; NSNumber *numInter = [[NSNumber alloc] initWithDouble:interval*1000]; long long llInter = numInter.longLongValue; //运动识别id self.actionId = [NSString stringWithFormat:@"%lld",llInter]; self.distance = 0.00f; self.second = 0; self.calorie = 0; self.step = 0; self.gpsDistance = 0.00f; self.agoGpsDistance = 0.00f; self.agoActionDistance = 0.00f; caidianCurrent.record_no = record_no; caidianCurrent.step = self.step; [self.arrSteps addObject:caidianCurrent]; [self.arrStepsSave addObject:caidianCurrent]; } else { int intervalCaidian = [caidianCurrent.date timeIntervalSinceDate:lastDate] * 1000; // 步行最大每秒2.5步,跑步最大每秒3.5步,超过此范围,数据有可能丢失 int min = 259; if (intervalCaidian >= min) { if (self.motionManager.isAccelerometerActive) { //存一下时间 lastDate = caidianCurrent.date; if (intervalCaidian >= ACCELERO_START_TIME * 1000) {// 计步器开始计步时间(秒) self.startStep = 0; } if (self.startStep < ACCELERO_START_STEP) {//计步器开始计步步数 (步) self.startStep ++; break; } else if (self.startStep == ACCELERO_START_STEP) { self.startStep ++; // 计步器开始步数 // 运动步数(总计) self.step = self.step + self.startStep; } else { self.step ++; } //步数在这里 NSLog(@"步数%d",self.step); int intervalMillSecond = [caidianCurrent.date timeIntervalSinceDate:[[self.arrSteps lastObject] date]] * 1000; if (intervalMillSecond >= 1000) { record_no++; caidianCurrent.record_no = record_no; caidianCurrent.step = self.step; [self.arrSteps addObject:caidianCurrent]; } // 每隔100步保存一条数据(将来插入DB用) VHSSteps *arrStepsSaveVHSSteps = (VHSSteps *)[self.arrStepsSave lastObject]; int intervalStep = caidianCurrent.step - arrStepsSaveVHSSteps.step; // DB_STEP_INTERVAL 数据库存储步数采集间隔(步) 100步 if (self.arrStepsSave.count == 1 || intervalStep >= DB_STEP_INTERVAL) { //保存次数 record_no_save++; caidianCurrent.record_no = record_no_save; [self.arrStepsSave addObject:caidianCurrent]; } } } } } } }]; } }@catch (NSException * e) { NSLog(@"Exception: %@", e); return; }
然后iPhone 5s出现了, 增加了 M7 运动协处理器,也带来了CMStepCounter类,从此我们就不用自己计算步数了,只要直接读取就好。
首先还是要检测协处理器是否可用
if (!([CMStepCounter isStepCountingAvailable] || [CMMotionActivityManager isActivityAvailable])) { NSString *msg = @"demo只支持iPhone5s以上机型."; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Opps!" message:msg delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; }
然后才是获取步数的方法,主要有两种:
计步 第一种方法
startStepCountingUpdatesToQueue:updateOn:withHandler:
开始分发当前步数计数数据到第三方应用
- (void)startStepCountingUpdatesToQueue:(NSOperationQueue *)queue updateOn:(NSInteger)stepCounts withHandler:(CMStepUpdateHandler)handler
Parameters
queue
被指定执行特定的handler块的操作队列。第三方可以指定一个定制队列或者使用操作队列协助app的主线程。该参数不能为nil
stepCounts
记录的步伐数据,达到该数值去执行handler块。该数值必须大于0
handler
该块在步伐计数达到或超出数值时会被执行,该参数不能为nil。更多块方法信息参考CMStepQueryHandler。
Discussion
该方法实现对用户步伐数据的追踪,并周期性地唤起块方法去分发结果。当第三方调用了该方法,步伐计数器会重置当前步伐数为0,并开始计数。每次计数到达指定的步伐数时,会执行指定的handler块方法。比如,当设定stepCounts为100时,会在100,200,300等数目时发送更新,激活该块方法。每次发送到该块方法的步伐数目都是从你调用该方法开始的步伐数目总和。
每次超过设定步数值时,指定的处理程序块handler会被执行。如果当超过设定值时第三方应用处在被挂起的状态,那程序块也不会被执行。当第三方应用被唤醒,程序块也不会执行,直到再次超过设定步数值。
可以调用stopStepCountingUpdates方法去停止分发步数计数,当然当步数计数对像被销毁的时候,分发过程也会被停止。
代码如下:
if ([CMStepCounter isStepCountingAvailable]) { self.stepCounter = [[CMStepCounter alloc] init]; [self.stepCounter startStepCountingUpdatesToQueue:self.operationQueue updateOn:1 withHandler: ^(NSInteger numberOfSteps, NSDate *timestamp, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { UIAlertView *error = [[UIAlertView alloc] initWithTitle:@"Opps!" message:@"error" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [error show]; } else { NSString *text = [NSString stringWithFormat:@"当前步数: %ld", (long)numberOfSteps]; //这里是步数 weakSelf.stepsLabel.text = text; } }); }]; }
计步 第二种方法
queryStepCountStartingFrom:to:toQueue:withHandler:
收集并返回某一时间段内的历史步数数据
- (void)queryStepCountStartingFrom:(NSDate *)start to:(NSDate *)end toQueue:(NSOperationQueue *)queuewithHandler:(CMStepQueryHandler)handler
Parameters
start
收集步数数据的开始时间,该参数不能为 nil.
end
收集步数数据的停止时间,该参数不能为nil.
queue
执行指定handler块的操作队列,第三方可以指定一个定制队列或者使用操作队列协助app的主线程。该参数不能为nil
handler
执行处理结果的块方法,该参数不能为nil。更多块方法信息参考CMStepQueryHandler。
Discussion
该方法为异步方法,会立即返回并且把结果分发到指定的handler块中处理。系统最多仅存储最近7天内的有效步数数据。如果在指定时间范围内没有数据,则会传递一个0值到handler块中。
代码如下
// 获取今日步数
__weak ViewController *weakSelf = self; self.operationQueue = [[NSOperationQueue alloc] init]; NSCalendar *calendar = [NSCalendar currentCalendar]; NSDate *now = [NSDate date]; NSDateComponents *components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:now]; // 开始日期 NSDate *startDate = [calendar dateFromComponents:components]; // 结束日期 NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0]; if ([CMStepCounter isStepCountingAvailable]) { [self.stepCounter queryStepCountStartingFrom:startDate to:endDate toQueue:self.operationQueue withHandler:^(NSInteger numberOfSteps, NSError * _Nullable error) { NSLog(@"%ld",numberOfSteps); dispatch_async(dispatch_get_main_queue(), ^{ if (error) { UIAlertView *error = [[UIAlertView alloc] initWithTitle:@"Opps!" message:@"error" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [error show]; } else { weakSelf.totalLabel.text = [NSString stringWithFormat:@"今日总步数%ld",numberOfSteps]; } }); }]; }
另外,iOS7还增加了CMMotionActivity类,用来获取运动状态
if ([CMMotionActivityManager isActivityAvailable]) { self.activityManager = [[CMMotionActivityManager alloc] init]; [self.activityManager startActivityUpdatesToQueue:self.operationQueue withHandler: ^(CMMotionActivity *activity) { dispatch_async(dispatch_get_main_queue(), ^{ NSString *status = [weakSelf statusForActivity:activity]; NSString *confidence = [weakSelf stringFromConfidence:activity.confidence]; weakSelf.statusLabel.text = [NSString stringWithFormat:@"状态: %@", status]; weakSelf.confidenceLabel.text = [NSString stringWithFormat:@"速度: %@", confidence]; }); }]; }
- (NSString *)statusForActivity:(CMMotionActivity *)activity { NSMutableString *status = @"".mutableCopy; if (activity.stationary) { [status appendString:@"not moving"]; } if (activity.walking) { if (status.length) [status appendString:@", "]; [status appendString:@"on a walking person"]; } if (activity.running) { if (status.length) [status appendString:@", "]; [status appendString:@"on a running person"]; } if (activity.automotive) { if (status.length) [status appendString:@", "]; [status appendString:@"in a vehicle"]; } if (activity.unknown || !status.length) { [status appendString:@"unknown"]; } return status; } - (NSString *)stringFromConfidence:(CMMotionActivityConfidence)confidence { switch (confidence) { case CMMotionActivityConfidenceLow: return @"Low"; case CMMotionActivityConfidenceMedium: return @"Medium"; case CMMotionActivityConfidenceHigh: return @"High"; default: return nil; } }
好吧,随着时间的推移,iOS8来了,也带来了healthkit,不过之前的方法满足需求也就还是用的CMStepCounter方法。
不过最近客户改需求了,手环,iWatch的数据也需要统计进来,就不得不用healthkit的方法了。
还是老套路,先检查能不能用
//查看healthKit在设备上是否可用,ipad不支持HealthKit if(![HKHealthStore isHealthDataAvailable]) { NSLog(@"设备不支持healthKit"); }
然后获取步数
//创建healthStore实例对象 self.healthStore = [[HKHealthStore alloc] init]; //设置需要获取的权限这里仅设置了步数 HKObjectType *stepCount = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount]; NSSet *healthSet = [NSSet setWithObjects:stepCount, nil]; //从健康应用中获取权限 [self.healthStore requestAuthorizationToShareTypes:nil readTypes:healthSet completion:^(BOOL success, NSError * _Nullable error) { if (success) { NSDateFormatter *formatter = [[NSDateFormatter alloc ]init]; [formatter setDateFormat:@"yyyy-MM-dd"]; NSDate *now = [NSDate date]; NSString *todaystr = [formatter stringFromDate:now]; NSDate *today = [formatter dateFromString:todaystr]; NSDate *next = [today dateByAddingTimeInterval:24*60*60];//定义需要获取的数据为步数
HKQuantityType *quantityType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
//设置获取的步数时间间隔
NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; dateComponents.day = 1; NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:today endDate:next options:HKQueryOptionStrictStartDate];//创建查询统计对象collectionQuery
HKStatisticsCollectionQuery *collectionQuery = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:predicate options: HKStatisticsOptionCumulativeSum | HKStatisticsOptionSeparateBySource anchorDate:[NSDate dateWithTimeIntervalSince1970:0] intervalComponents:dateComponents]; collectionQuery.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection * __nullable result, NSError * __nullable error) { float numberOfSteps = 0; for (HKStatistics *statistic in result.statistics) { for (HKSource *source in statistic.sources) { //HKSource
对象中的name
可用于区分健康数据来源 if ([source.name isEqualToString:deviceName]) { float steps = [[statistic sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]]; numberOfSteps += steps; }
//deviceName是根据接入的设备做的标记, if ([deviceName isEqualToString:@"iPhone"]) { if ([source.name isEqualToString:[UIDevice currentDevice].name]) { float steps = [[statistic sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]]; numberOfSteps += steps; } }else if ([deviceName isEqualToString:@"iWatch"] && ![source.name isEqualToString:[UIDevice currentDevice].name]){ if ([source.bundleIdentifier hasPrefix:@"com.apple.health"]) { float steps = [[statistic sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]]; numberOfSteps += steps; } }else if ([deviceName isEqualToString:@"xiaomi"]){ if ([source.name isEqualToString:@"小米运动"] || [source.bundleIdentifier isEqualToString:@"HM.wristband"]) { float steps = [[statistic sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]]; numberOfSteps += steps; } } } } NSLog(@"ff = %f",numberOfSteps); stepString = [NSString stringWithFormat:@"%.0f",numberOfSteps]; }; [self.healthStore executeQuery:collectionQuery]; } else { NSLog(@"获取步数权限失败"); } }];
demo完整代码在这里: