最近开始跑步了,每天看到「健身纪录」的圆圈,挺有感触的。

IMG_0248

每天的「圈」里能看到当天的活动量、锻炼时长、站立时间。

打开详情后,还能看到跑步步数、跑步距离、以及从第三方同步过来的数据等。

IMG_0254

今天我们的目标是拿到第一个量化数据:「健身记录」的圆圈数据,即 iPhone 提供的 HealthKit data 数据导出,放入第三方数据库中,以供后续统计和分析。

数据来源

简单看看我的「健康」APP 的数据:
IMG_0259

有了健康 app,你可以将各种健康和健身信息都保存在一个地方,让它们尽在你的掌控。保存哪些信息,以及哪些 app 可以通过健康 app 访问你的数据都由你决定。当你使用密码、触控 ID 或面容 ID 锁定手机时,健康 app 中所有的健康和运动数据都将被加密,只有医疗急救卡中的信息除外。你可以通过 iCloud,让健康数据自动在你的各种设备上保持更新,包括传输和存储过程都会被加密保护。此外,访问 HealthKit 的 app 都必须备有隐私政策,因此在授权这些 app 访问你的健康和健身数据之前,请务必仔细查看这些政策。

我们再来看看平时都有哪些第三方 app 授权访问我们的健康数据:

IMG_0255

数据导出

数据来源有了,接下来就是考虑如何将健康数据导出。通过对 HealthKit 的了解,我们需要自己动手开发一个 iOS app,获取健康数据,并上传到我们的服务器上,达到数据导出的目标。

IMG_0250

这张「圈」图是我 11 月 9 日的数据,今天的目标就是通过 HealthKit 获取这个圈的数据,主要包括:

  1. 活动数据:1048 大卡
  2. 锻炼时间:92 分钟
  3. 站立时间:11 小时

开发准备

在创建 iOS 应用 id 时,需要拥有「HealthKit」能力:

Info.plist 配置中增加以下两项内容:

MBHealthTracker

参考 HealthKit 开发文档,要获取「圈」运动统计数据,需要利用 Activity summary query 即,HKActivitySummaryQuery 查询。

结果字段类型为:HKActivitySummary

HKActivitySummaryQuery

A query for read activity summary objects from the HealthKit store.

参考:https://developer.apple.com/documentation/healthkit/hkactivitysummaryquery

HKActivitySummary

An object that contains the move, exercise, and stand data for a given day.

参考:https://developer.apple.com/documentation/healthkit/hkactivitysummary

了解了 HKActivitySummaryQueryHKActivitySummary,我们就可以进入开发了。

这里我主要借助「MBHealthTracker」,MBHealthTracker 封装好了和 HealthKit 交互的授权,获取数据等,只要直接调用即可。

MBHealthTracker Github: https://github.com/matybrennan/MBHealthTracker

当我发现 MBHealthTracker 没有对应的功能获取 HKActivitySummary 功能,所以需要我们在 MBHealthTracker 基础上增加杜对应的获取方法。

ActivitySummaryService

先在 Presentation 下增加 ActivitySummary 模型,主要是创建数组。

import Foundation
import HealthKit

public struct ActivitySummary {
    public let items: [HKActivitySummary]
}

接着在 Business Logic 逻辑层,创建协议和实现方法:

// ActivitySummaryServiceProtocol.swift
import Foundation
import HealthKit

public protocol ActivitySummaryServiceProtocol {
    
    // 根据起止时间获取 ActivitySummary
    func getActivitySummary(startDate: Date, endDate: Date, completionHandler: @escaping (MBAsyncCallResult<ActivitySummary>) -> Void) throws
}

// ActivitySummaryService.swift
import Foundation
import HealthKit

class ActivitySummaryService {
    public init() { }
}

extension ActivitySummaryService: ActivitySummaryServiceProtocol {
    
    func getActivitySummary(startDate: Date, endDate: Date, completionHandler: @escaping (MBAsyncCallResult<ActivitySummary>) -> Void) throws {
        
        
        try isDataStoreAvailable()
        
        // Create the date components for the predicate
        guard let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian) else {
            fatalError("*** This should never fail. ***")
        }
        
        let units: NSCalendar.Unit = [.day, .month, .year, .era]
        
        var startDateComponents = calendar.components(units, from: startDate)
        startDateComponents.calendar = calendar as Calendar
        
        var endDateComponents = calendar.components(units, from: endDate)
        endDateComponents.calendar = calendar as Calendar
        
        // Create the predicate for the query
        let summariesWithinRange = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents, end: endDateComponents)
         
        // Build the query
        let query = HKActivitySummaryQuery(predicate: summariesWithinRange) { (query, summaries, error) -> Void in
            self.configure(query: query, summaries: summaries, error: error, completionHandler: completionHandler)
        }
        
        healthStore.execute(query)
    }
}

private extension ActivitySummaryService {
    
    func configure(query: HKActivitySummaryQuery, summaries: [HKActivitySummary]?, error: Error?, completionHandler: @escaping (MBAsyncCallResult<ActivitySummary>) -> Void) {
        
        guard error == nil else {
            completionHandler(.failed(error!))
            return
        }
        let activitySummary = ActivitySummary(items: summaries!)
        completionHandler(.success(activitySummary))
    }
}

参考官网的 demo 和 MBHealthTracker 的方法,依葫芦画瓢写好实现类,代码简单,就不详细说明。

剩下的就是在 MBHealthTrackerProtocolMBHealthTracker 载入方法即可。

// MBHealthTrackerProtocol.swift
var activitySummary: ActivitySummaryServiceProtocol { get }

// MBHealthTracker.swift
private lazy var privateActivitySummaryService: ActivitySummaryServiceProtocol = {
    return ActivitySummaryService()
}()

...

public var activitySummary: ActivitySummaryServiceProtocol {
    return privateActivitySummaryService
}

获取数据的部分暂时告一段落,下一步就是要通过配置,拿到授权可以读取 ActivitySummary 数据。

/// Just has read capabilities
public enum MBReadType: ReadableType {
    
    // Characteristics
    case dob
    case gender
    case activitySummary
    
    public var readable: HKObjectType {
        switch self {
        case .dob:
            return HKCharacteristicType.characteristicType(forIdentifier: .dateOfBirth)!
        case .gender:
            return HKCharacteristicType.characteristicType(forIdentifier: .biologicalSex)!
        case .activitySummary: return HKActivitySummaryType.activitySummaryType()
        }
    }
}

万事俱备,我们测试下看看运行结果。

import Foundation
import HealthKit

protocol ViewInteractorProtocol {
    func configurePermissions()
    func runTest()
}

class ViewInteractor {
    
    private let healthTracker: MBHealthTrackerProtocol
    
    init(healthTracker: MBHealthTrackerProtocol) {
        self.healthTracker = healthTracker
    }
}

// MARK: - ViewInteractorProtocol
extension ViewInteractor: ViewInteractorProtocol {
    
    // 配置写入和读取授权的类目
    func configurePermissions() {
        healthTracker.configuration.requestAuthorization(toShare: []
        
        ,toRead: [MBReadType.activitySummary]) { _ in }
    }
    
    // 测试,看看11-9号到11-3号得数据
    func runTest() {
        do {
            print("-----------------get summary begin----------------")
            let date = Date()
            let start = date.parse("2019-11-09")
            let end = date.parse("2019-11-13")
            try healthTracker.activitySummary.getActivitySummary(startDate: start, endDate: end, completionHandler: { (result) in
                print(result)
            })
            print("-----------------get summary end----------------")
        } catch {
            print("Unable to get: \(error.localizedDescription)")
        }
    }
}

运行打印出来的结果:

success(Lianghua.ActivitySummary(items: [<<HKActivitySummary: 0x2832480c0>: Date=(Year: 2019, Month: 11, Day: 9) Active Energy Burned=(1048.476259360438/310) Apple Exercise Minutes=(92/30) Apple Stand Hours=(11/12)>, <<HKActivitySummary: 0x283248180>: Date=(Year: 2019, Month: 11, Day: 10) Active Energy Burned=(474.8101270220084/310) Apple Exercise Minutes=(33/30) Apple Stand Hours=(6/12)>, <<HKActivitySummary: 0x283240f00>: Date=(Year: 2019, Month: 11, Day: 11) Active Energy Burned=(357.55/310) Apple Exercise Minutes=(22/30) Apple Stand Hours=(16/12)>, <<HKActivitySummary: 0x283241200>: Date=(Year: 2019, Month: 11, Day: 12) Active Energy Burned=(344.8089999999997/310) Apple Exercise Minutes=(17/30) Apple Stand Hours=(16/12)>, <<HKActivitySummary: 0x2832412c0>: Date=(Year: 2019, Month: 11, Day: 13) Active Energy Burned=(181.595/310) Apple Exercise Minutes=(21/30) Apple Stand Hours=(4/12)>]))

这里我们对照开篇的「圈」图,和这里的数据完全一致。

总结

我们导出健康数据后,就要考虑统一存放到云平台或者第三方存储平台上,以供后续统计分析。具体选择什么平台来存储数据呢,我们下期再聊!

推荐

  1. 自我量化开篇:https://ziwolianghua.coding01.cn/1.1.html

未完待续

参考:

  1. https://developer.apple.com/documentation/healthkit
  2. https://github.com/openmhealth/Granola
  3. https://www.openmhealth.org/
  4. https://www.apple.com/cn/ios/health/
  5. https://github.com/mseemann/healthkit-sample-generator
  6. https://github.com/matybrennan/MBHealthTracker
  7. https://www.openmhealth.org/features/case-studies/
  8. https://developer.apple.com/documentation/healthkit/hkactivitysummaryquery

Coding01
831 声望123 粉丝