最近开始跑步了,每天看到「健身纪录」的圆圈,挺有感触的。
每天的「圈」里能看到当天的活动量、锻炼时长、站立时间。
打开详情后,还能看到跑步步数、跑步距离、以及从第三方同步过来的数据等。
今天我们的目标是拿到第一个量化数据:「健身记录」的圆圈数据,即 iPhone 提供的 HealthKit data 数据导出,放入第三方数据库中,以供后续统计和分析。
数据来源
简单看看我的「健康」APP 的数据:
有了健康 app,你可以将各种健康和健身信息都保存在一个地方,让它们尽在你的掌控。保存哪些信息,以及哪些 app 可以通过健康 app 访问你的数据都由你决定。当你使用密码、触控 ID 或面容 ID 锁定手机时,健康 app 中所有的健康和运动数据都将被加密,只有医疗急救卡中的信息除外。你可以通过 iCloud,让健康数据自动在你的各种设备上保持更新,包括传输和存储过程都会被加密保护。此外,访问 HealthKit 的 app 都必须备有隐私政策,因此在授权这些 app 访问你的健康和健身数据之前,请务必仔细查看这些政策。
我们再来看看平时都有哪些第三方 app 授权访问我们的健康数据:
数据导出
数据来源有了,接下来就是考虑如何将健康数据导出。通过对 HealthKit 的了解,我们需要自己动手开发一个 iOS app,获取健康数据,并上传到我们的服务器上,达到数据导出的目标。
这张「圈」图是我 11 月 9 日的数据,今天的目标就是通过 HealthKit 获取这个圈的数据,主要包括:
- 活动数据:1048 大卡
- 锻炼时间:92 分钟
- 站立时间: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
了解了 HKActivitySummaryQuery
和 HKActivitySummary
,我们就可以进入开发了。
这里我主要借助「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 的方法,依葫芦画瓢写好实现类,代码简单,就不详细说明。
剩下的就是在 MBHealthTrackerProtocol
和 MBHealthTracker
载入方法即可。
// 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)>]))
这里我们对照开篇的「圈」图,和这里的数据完全一致。
总结
我们导出健康数据后,就要考虑统一存放到云平台或者第三方存储平台上,以供后续统计分析。具体选择什么平台来存储数据呢,我们下期再聊!
推荐
未完待续
参考:
- https://developer.apple.com/documentation/healthkit
- https://github.com/openmhealth/Granola
- https://www.openmhealth.org/
- https://www.apple.com/cn/ios/health/
- https://github.com/mseemann/healthkit-sample-generator
- https://github.com/matybrennan/MBHealthTracker
- https://www.openmhealth.org/features/case-studies/
- https://developer.apple.com/documentation/healthkit/hkactivitysummaryquery