Just My Life & My Work

原本想透過智慧運動裝置來獲取步數與距離,可惜遲遲等不到韌體開發完成,只好暫且透過HealthKit獲取步數與距離!因為iPhone本身就有運動感測器,會自動算出步數與距離,然後寫入HealthKit,想做運動健康相關App,於是有數據來源可使用。

iOS 10開始之後要在info.plist設定Privacy

接下來看程式碼,我寫成TPHealthKitManager

/**
 Theme: HealthKit Get Step and Distance
 IDE: Xcode 9
 Language: Objective C
 Date: 107/08/13
 Author: HappyMan
 Blog: https://cg2010studio.com/
 */
// TPHealthKitManager.h
#import <Foundation/Foundation.h>
#import <HealthKit/HealthKit.h>
#import <UIKit/UIDevice.h>

#define HKVersion [[[UIDevice currentDevice] systemVersion] doubleValue]
#define CustomHealthErrorDomain @"com.happystudio.healthError"

@interface TPHealthKitManager : NSObject

@property (nonatomic, strong) HKHealthStore *healthStore;

+(TPHealthKitManager *)sharedManager;
- (void)authorizeHealthKit:(void(^)(BOOL success, NSError *error))compltion;
- (void)getStepCount:(void(^)(double value, NSError *error))completion;
- (void)getDistance:(void(^)(double value, NSError *error))completion;

@end

// TPHealthKitManager.m
#import "TPHealthKitManager.h"

@implementation TPHealthKitManager

+(TPHealthKitManager *)sharedManager
{
    static TPHealthKitManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[TPHealthKitManager alloc] init];
    });
    
    return manager;
}

/*
 *  @brief  檢查是否支持獲取健康數據
 */
- (void)authorizeHealthKit:(void(^)(BOOL success, NSError *error))compltion
{
    if(HKVersion >= 8.0)
    {
        if (![HKHealthStore isHealthDataAvailable]) {
            NSError *error = [NSError errorWithDomain: @"com.happystudio.smartpatch" code: 2 userInfo: [NSDictionary dictionaryWithObject:@"HealthKit is not available in this Device"                                                                      forKey:NSLocalizedDescriptionKey]];
            if (compltion != nil) {
                compltion(false, error);
            }
            return;
        }
        if ([HKHealthStore isHealthDataAvailable]) {
            if(self.healthStore == nil)
                self.healthStore = [[HKHealthStore alloc] init];
            /*
             組裝需要讀寫的數據類型
             */
            NSSet *writeDataTypes = [self dataTypesToWrite];
            NSSet *readDataTypes = [self dataTypesRead];
            
            /*
             註冊需要讀寫的數據類型,也可以在“健康”APP中重新修改
             */
            [self.healthStore requestAuthorizationToShareTypes:writeDataTypes readTypes:readDataTypes completion:^(BOOL success, NSError *error) {
                
                if (compltion != nil) {
                    NSLog(@"error->%@", error.localizedDescription);
                    compltion (success, error);
                }
            }];
        }
    }
    else {
        NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"iOS 系統低於8.0"                                                                      forKey:NSLocalizedDescriptionKey];
        NSError *aError = [NSError errorWithDomain:CustomHealthErrorDomain code:0 userInfo:userInfo];
        compltion(0, aError);
    }
}

/*!
 *  @brief  寫權限
 *  @return 集合
 */
- (NSSet *)dataTypesToWrite
{
    HKQuantityType *heightType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeight];
    HKQuantityType *weightType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyMass];
    HKQuantityType *temperatureType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyTemperature];
    HKQuantityType *activeEnergyType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierActiveEnergyBurned];
    
    return [NSSet setWithObjects:heightType, temperatureType, weightType, activeEnergyType, nil];
}

/*!
 *  @brief  讀權限
 *  @return 集合
 */
- (NSSet *)dataTypesRead
{
    HKQuantityType *heightType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeight];
    HKQuantityType *weightType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyMass];
    HKQuantityType *temperatureType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierBodyTemperature];
    HKCharacteristicType *birthdayType = [HKObjectType characteristicTypeForIdentifier:HKCharacteristicTypeIdentifierDateOfBirth];
    HKCharacteristicType *sexType = [HKObjectType characteristicTypeForIdentifier:HKCharacteristicTypeIdentifierBiologicalSex];
    HKQuantityType *stepCountType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    HKQuantityType *distance = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierDistanceWalkingRunning];
    HKQuantityType *activeEnergyType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierActiveEnergyBurned];
    
    return [NSSet setWithObjects:heightType, temperatureType, birthdayType, sexType, weightType, stepCountType, distance, activeEnergyType,nil];
}

// 獲取步數
- (void)getStepCountWithDate:(NSDate *)date completion:(void(^)(double value, NSError *error))completion
{
    HKQuantityType *stepType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
    
    // Since we are interested in retrieving the user's latest sample, we sort the samples in descending order, and set the limit to 1. We are not filtering the data, and so the predicate is set to nil.
    HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:stepType predicate:[TPHealthKitManager predicateForSamplesWithDate:date] limit:HKObjectQueryNoLimit sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
        if(error)
        {
            completion(0,error);
        }
        else
        {
            NSInteger totalSteps = 0;
            for(HKQuantitySample *quantitySample in results)
            {
                HKQuantity *quantity = quantitySample.quantity;
                HKUnit *countUnit = [HKUnit countUnit];
                double userSteps = [quantity doubleValueForUnit:countUnit];
                totalSteps += userSteps;
            }
            NSLog(@"當天行走步數 = %ld",(long)totalSteps);
            completion(totalSteps, error);
        }
    }];
    
    [self.healthStore executeQuery:query];
}

// 獲取公里數
- (void)getDistanceWithDate:(NSDate *)date completion:(void(^)(double value, NSError *error))completion
{
    HKQuantityType *distanceType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierDistanceWalkingRunning];
    NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
    HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:distanceType predicate:[TPHealthKitManager predicateForSamplesWithDate:date] limit:HKObjectQueryNoLimit sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        
        if(error)
        {
            completion(0, error);
        }
        else
        {
            double totalDistance = 0;
            for(HKQuantitySample *quantitySample in results)
            {
                HKQuantity *quantity = quantitySample.quantity;
                HKUnit *distanceUnit = [HKUnit meterUnitWithMetricPrefix:HKMetricPrefixKilo];
                double userDistance = [quantity doubleValueForUnit:distanceUnit];
                totalDistance += userDistance;
            }
            NSLog(@"當天行走距離 = %.2f", totalDistance);
            completion(totalDistance, error);
        }
    }];
    
    [self.healthStore executeQuery:query];
}

// 獲取動態卡路里
- (void)getCaloryWithDate:(NSDate *)date completion:(void(^)(double value, NSError *error))completion
{
    HKQuantityType *caloryType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierActiveEnergyBurned];
    NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
    HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:caloryType predicate:[TPHealthKitManager predicateForSamplesWithDate:date] limit:HKObjectQueryNoLimit sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        
        if(error)
        {
            completion(0, error);
        }
        else
        {
            double totalCalory = 0;
            for(HKQuantitySample *quantitySample in results)
            {
                HKQuantity *quantity = quantitySample.quantity;
                HKUnit *caloryUnit = [HKUnit largeCalorieUnit];
                double userCalory = [quantity doubleValueForUnit:caloryUnit];
                totalCalory += userCalory;
            }
            NSLog(@"當天動態卡路里 = %.2f", totalCalory);
            completion(totalCalory, error);
        }
    }];
    
    [self.healthStore executeQuery:query];
}

/*!
 *  @brief  某天時間段
 *
 *  @return 時間段
 */
+ (NSPredicate *)predicateForSamplesWithDate:(NSDate *)date
{
    NSCalendar *calendar = [NSCalendar currentCalendar];
//    NSDate *now = [NSDate date];
    NSDateComponents *components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:date];
    [components setHour:0];
    [components setMinute:0];
    [components setSecond: 0];
    
    NSDate *startDate = [calendar dateFromComponents:components];
    NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
    NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];
    return predicate;
}

// 使用方式
-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [self getStepCount];
    [self getDistance];
    [self getCalory];
}


-(void)getStepCount
{
    TPHealthKitManager *healthkitManager = [TPHealthKitManager sharedManager];
    [healthkitManager authorizeHealthKit:^(BOOL success, NSError *error) {
        
        if (success) {
            NSLog(@"Success");
            [healthkitManager getStepCountWithDate:[NSDate date] completion:^(double value, NSError *error) {
                NSLog(@"1count-->%.0f", value);
                NSLog(@"1error-->%@", error.localizedDescription);
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"步數:%.0f步", value);
                    stepLabel.text = [NSString stringWithFormat:@"%.0f", value];
                    stepMainLabel.text = [NSString stringWithFormat:@"%.0f", value];

                    [self setupPaint];
                });
                
            }];
        }
        else {
            NSLog(@"Fail");
        }
    }];
}

-(void)getDistance
{
    TPHealthKitManager *healthkitManager = [TPHealthKitManager sharedManager];
    [healthkitManager authorizeHealthKit:^(BOOL success, NSError *error) {
        
        if (success) {
            NSLog(@"Success");
            [healthkitManager getDistanceWithDate:[NSDate date] completion:^(double value, NSError *error) {
                NSLog(@"2count-->%.2f", value);
                NSLog(@"2error-->%@", error.localizedDescription);
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"公里數:%.2f公里", value);
                    
                    distanceLabel.text = [NSString stringWithFormat:@"%.2f", value];
                });
                
            }];
        }
        else {
            NSLog(@"Fail");
        }
    }];
}

-(void)getCalory
{
    TPHealthKitManager *healthkitManager = [TPHealthKitManager sharedManager];
    [healthkitManager authorizeHealthKit:^(BOOL success, NSError *error) {
        
        if (success) {
            NSLog(@"Success");
            [healthkitManager getCaloryWithDate:[NSDate date] completion:^(double value, NSError *error) {
                NSLog(@"3count-->%.2f", value);
                NSLog(@"3error-->%@", error.localizedDescription);
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"動態卡路里:%.2f大卡", value);
                    
                    caloryLabel.text = [NSString stringWithFormat:@"%.2f", value];
                });
                
            }];
        }
        else {
            NSLog(@"Fail");
        }
    }];
}

列印出來:

error->(null)
Success
當天行走步數 = 9780
1count–>9780
1error–>(null)
步數:9780步
error->(null)
Success
error->(null)
Success
當天行走距離 = 6.72
2count–>6.72
2error–>(null)
公里數:6.72公里
當天動態卡路里 = 269.92
3count–>269.92
3error–>(null)
動態卡路里:269.92大卡

我介面做成如下圖~

我心想是不是有算錯,我看手環也才6000多步,怎麼會回傳將近10000步!?Trace Code後發現,原來我有多個裝置都會儲存步數:P~所以要再分別拿取個別裝置來加總,如此一來就會是正確的數字囉~此時也可以比較是手環還是手錶步數較準確。

當我讀取步數或距離時,會有四個裝置的資料:Apple Watch、小米運動、iPhone 6和iPhone 6S+,因為這幾個同時綁定在我的帳戶,只要透過HealthKit,就能把資料都匯入健康App中,若我們的App想要存取HealthKit的數據,就必須請使用者同意權限。

我將抓到的數據稍微分析一下,可知道數據每次存入的期間不太一樣,這就要看程式怎麼寫。Apple Watch是每一分鐘寫入一次,而iPhone則是每10分鐘寫入一次。

既然我隨性配戴如此多「運動感應器」,想要怎麼取資料就看我的用圖,有時後上個廁所沒帶iPhone,這期間就可以拿Apple Watch的資料來補。

步數

距離

小米運動似乎沒有把「距離」寫入HealthKit,所以我沒有抓到⋯⋯

接下來就可以去存取其他健康數據,比如越來越夯的心率變異數

參考:iOS 通過HealthKit框架獲取步數和步行+跑步距離iOS 使用HealthKit框架實現獲取今日步數

Comments on: "[iOS] 透過HealthKit獲取步數與距離" (2)

  1. 謝謝您分享的coding. 🙏

    Liked by 1 person

隨意留個言吧:)~

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

標籤雲