HealthKit教程 Swift版:锻炼信息
原文:HealthKit Tutorial with Swift: Workouts 作者:Ernesto García 译者:Mr_cyz )
欢迎回到我们的HealthKit系列教程!
在我们系列教程的第一篇(中译版)中,你已经学到了使用HealthKit开发的基础:读写数据。
在这第二篇,同时也是最后一篇中,你将会学到怎么样处理一种更复杂的数据类型:锻炼与健身的信息(Workout)。
这篇教程从上一篇教程结束的地方开始,所以如果你还没有上一篇中完成的工程,你可以从这里下载。
开始
单从身体方面来说,一个人的锻炼与健身信息由一段时间内做过的身体素质锻炼组成。而再加上数字化的方面来看,你可以通过下面的这些基本属性来得到一条锻炼信息:
-
运动的种类。例如:跑步、骑行、冰壶等
-
距离
-
起止时间
-
持续时间
-
运动时消耗的能量。
数字化的领域就是指的HealthKit了,一条锻炼信息就是一个其他类型的数据采样信息(samples)的容器。例如,你可以添加一组数据,来表示在你运动时的心率。如果你打算做一款健康类型的app,那么这将是一个功能强大的特性。
在该工程中,你将会存储跑步锻炼时的信息,当然你也可以很容易地改变活动的种类来表示其他类型的锻炼信息。
我们的起始项目中已经包含了一个视图控制器,来为你提供查看健康信息的入口。导航到Workouts栏然后点击+按钮你就能看到了。
当你停止运动时,会在该页面收集信息并且展示到Workouts视图控制器中。你需要使用这些信息来创建一条健康信息。
保存锻炼信息
首先,你将创建一个方法来保存跑步时的信息。打开HealthManager.swift,添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | func saveRunningWorkout(startDate:NSDate , endDate:NSDate , distance:Double, distanceUnit:HKUnit , kiloCalories:Double, completion: ( (Bool, NSError!) -> Void)!) { // 1. Create quantities for the distance and energy burned let distanceQuantity = HKQuantity(unit: distanceUnit, doubleValue: distance) let caloriesQuantity = HKQuantity(unit: HKUnit.kilocalorieUnit(), doubleValue: kiloCalories) // 2. Save Running Workout let workout = HKWorkout(activityType: HKWorkoutActivityType.Running, startDate: startDate, endDate: endDate, duration: abs(endDate.timeIntervalSinceDate(startDate)), totalEnergyBurned: caloriesQuantity, totalDistance: distanceQuantity, metadata: nil) healthKitStore.saveObject(workout, withCompletion: { (success, error) -> Void in if ( error != nil ) { // Error saving the workout completion(success,error) } else { // Workout saved completion(success,nil) } }) } |
这段代码做了什么呢?让我们一行一行地分析:
-
与你之前创建BMI类型的身体素质信息一样,创建两个身体素质类型的对象,分别设置为距离和能量类型,注意使用double类型的数据以及选择合适的单位。
-
使 用起止时间、持续时间以及你刚刚创建好的代表距离和消耗的能量的身体素质信息来创建一个HKWorkOut对象,然后通过调用HKHealthKit Store的saveObject方法来将健康信息保存到Store中,结果信息或者是错误信息将传到completion回调中。
现在你需要在Workouts视图控制器中调用该方法,打开WorkoutsTableViewController.swift然后找到unwindToSegue()方法,当你在新的Workout界面按下Done的时候就会调用该方法。
将下面这一行:
1 | println( "TODO: Save workout in Health Store" ) |
替换为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | if let addViewController:AddWorkoutTableViewController = segue.sourceViewController as? AddWorkoutTableViewController { // 1. Set the Unit type var hkUnit = HKUnit.meterUnitWithMetricPrefix(.Kilo) if distanceUnit == .Miles { hkUnit = HKUnit.mileUnit() } // 2. Save the workout self.healthManager?.saveRunningWorkout(addViewController.startDate!, endDate: addViewController.endDate!, distance: addViewController.distance , distanceUnit:hkUnit, kiloCalories: addViewController.energyBurned!, completion: { (success, error ) -> Void in if ( success ) { println( "Workout saved!" ) } else if ( error != nil ) { println( "\(error)" ) } }) } |
-
首先,创建一个合适的单位对象,用户通过分段控件(segment control)设置distanceUnit的值,从而选择距离单位的类型。这段代码检查distanceUnit的值来决定使用合适的HKUnit。
-
创建完单位之后调用saveRunningWorkoutMethod()方法来保存这些健康信息,包括开始日期、结束日期、持续时间以及能量消耗。
编译并运行,点击 + 按钮,像下面视图中展示的那样填入数据。
哇!26.2英里(42.195千米),而且是两小时零一分钟之内,我想你刚刚在编码的过程中打破了马拉松世界记录。你真是个天才!
完成之后点击Done,如果一切顺利,你将在Xcode的控制台中看到如下信息:
1 | Workout saved! |
太棒了!你的健康信息已经成功地被保存到HealthKit Store中了,如果你想的话你可以重复刚才的操作来添加更多的健康信息。
查询健康信息
如果你运行你的应用,然后进入Workouts视图控制器,你不会看到任何之前你在视图中创建好的健康信息。
你需要加入一些代码来读取并且展示这些信息。为了能够读取到这些信息,你需要创建一个HKSampleQuery对象,然后执行这个查询来获取数据。
这与之前读取身高和体重的代码将会非常类似,为什么试着自己写一下呢?
在HealthManager.swift中创建一个方法,利用HKWorkoutActivityType类型来查询健康信息,将结果按照起始日期降序的顺序排好序,在completion回调中获取返回结果。使用如下方法声明:
1 | func readRunningWorkOuts(completion: (([AnyObject]!, NSError!) -> Void)!) |
内部实现:readRunningWorkOuts的实现:
打开HealthManager.swift然后添加这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | func readRunningWorkOuts(completion: (([AnyObject]!, NSError!) -> Void)!) { // 1. Predicate to read only running workouts let predicate = HKQuery.predicateForWorkoutsWithWorkoutActivityType(HKWorkoutActivityType.Running) // 2. Order the workouts by date let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false ) // 3. Create the query let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor]) { (sampleQuery, results, error ) -> Void in if let queryError = error { println( "There was an error while reading the samples: \(queryError.localizedDescription)" ) } completion(results,error) } // 4. Execute the query healthKitStore.executeQuery(sampleQuery) } |
这段代码与你之前读身高和体重的代码非常类似:
-
首 先创建一个谓词对象,HKQuery提供了一个方法:predicateForWorkoutsWithWorkoutActivityType()来创 建查询锻炼信息用到的谓词。通过使用HKWorkoutActivityType.Running参数来指定你希望查询的是跑步类型的锻炼信息。如果你想 查询其他类型的锻炼信息,例如骑行、游泳,你只需要在创建该谓词的时候改变查询类型即可。
-
创建一个排序标识符来让结果信息按照日期降序排列。
-
创建HKSampleQuery对象,调用executeQuery()方法来获得结果。
你需要将健康信息展示到列表中,所以你要调用该方法并实现UITableView的数据源协议,所以,接下来打开WorkoutsTableViewController.swift。
你需要在该类中创建一个数组类型的属性来保存所有的健康信息,将这一行代码添加到WorkoutsTableViewController顶部声明其他属性的附近:
1 | var workouts = [HKWorkout]() |
然后,添加该方法,作用是当视图刚刚出现时读取健康信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public override func viewWillAppear(animated: Bool) { super .viewWillAppear(animated) healthManager?.readRunningWorkOuts({ (results, error) -> Void in if ( error != nil ) { println( "Error reading workouts: \(error.localizedDescription)" ) return ; } else { println( "Workouts read successfully!" ) } //Kkeep workouts and refresh tableview in main thread self.workouts = results as [HKWorkout] dispatch_async(dispatch_get_main_queue(), { () -> Void in self.tableView.reloadData() }); }) } |
这里调用了你刚刚创建好的方法readWorkouts。当收到结果后,将结果保存到workouts中,然后在主线程中刷新列表数据。
现在,你需要添加列表的数据源协议中的方法,在WorkoutsTableViewController中添加tableView:numberOfRowsInSection方法的实现:
1 2 3 | public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return workouts.count } |
这很直接明了,当列表询问有多少行时,你只需要返回你从Store中读到的健康信息的数量即可。
现在是时候填充列表的cell了,你需要实现在列表的数据源协议中的方法tableView:cellForRowAtIndexPath,将如下代码添加到WorkoutsTableViewController类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier( "workoutcellid" , forIndexPath: indexPath) as UITableViewCell // 1. Get workout for the row. Cell text: Workout Date let workout = workouts[indexPath.row] let startDate = dateFormatter.stringFromDate(workout.startDate) cell.textLabel!.text = startDate // 2. Detail text: Duration - Distance // Duration var detailText = "Duration: " + durationFormatter.stringFromTimeInterval(workout.duration)! // Distance in Km or miles depending on user selection detailText += " Distance: " if distanceUnit == .Kilometers { let distanceInKM = workout.totalDistance.doubleValueForUnit(HKUnit.meterUnitWithMetricPrefix(HKMetricPrefix.Kilo)) detailText += distanceFormatter.stringFromValue(distanceInKM, unit: NSLengthFormatterUnit.Kilometer) } else { let distanceInMiles = workout.totalDistance.doubleValueForUnit(HKUnit.mileUnit()) detailText += distanceFormatter.stringFromValue(distanceInMiles, unit: NSLengthFormatterUnit.Mile) } // 3. Detail text: Energy Burned let energyBurned = workout.totalEnergyBurned.doubleValueForUnit(HKUnit.jouleUnit()) detailText += " Energy: " + energyFormatter.stringFromJoules(energyBurned) cell.detailTextLabel?.text = detailText; return cell } |
解释一下上面的代码:
-
这段代码获得当前这一行的健康信息,然后将起始日期格式化并展示在cell中的text lable上。为了将日期格式化,这里使用了之前在初始工程中为你创建好的NSDateFormatter类。
-
通 过距离和消耗的能量来定义展示在detail label中的字符串。距离信息以英里或者千米的形式展示出来,这取决于用户的选择(被保存在distanceUnit属性中)。获取距离的double 类型的值,然后基于该属性的值传入一个合适的距离单位。接下来,使用NSDistanceFormatter类,传入合适的单位,调用 stringFromValue:unit方法,将距离信息格式化为本地字符串。对于该条健康信息的持续时间,使用一个 NSDateComponentsFormatter类对象。所有这些格式转换器都是在初始工程中为你预先创建好的。
-
使用NSEnergyFormatter将消耗的能量也转换为字符串,该字符串最终将被展示到detail label中。
编译并运行该app,导航到Workouts页面,现在你应该能在列表中看到之前储存好的健康信息了。
酷!你已经如期将所有的健康信息展示出来了。现在,点击分段控件,检验一下,距离以英里或以千米为单位来展示,取决于你的选择。
如果你打开Health应用,你不会在任何地方找到这些信息,它就是这样设计的,因为Health应用只展示数据采样信息,并不会展示我们的健康信息。
然而,你是可以让用户看到这些关于健康的信息的,你只需要将它们与一些采样信息关联起来即可。一款健康管家应用如果不带有健康与锻炼的信息,就好比一教练机没有警报装置一样。因此你应该将这些数据结合起来。
把采样信息添加到健康信息中
作为最后一步,你将把距离和消耗的能量这些采样信息添加到健康信息中。
打开HealthManager.swift然后前往saveRunningWorkout(),在成功执行的回调闭包中,将下面这两行:
1 2 | // Workout saved completion(success,nil) |
替换为:
1 2 3 4 5 6 7 | // if success, then save the associated samples so that they appear in the HealthKit let distanceSample = HKQuantitySample(type: HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning), quantity: distanceQuantity, startDate: startDate, endDate: endDate) let caloriesSample = HKQuantitySample(type: HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierActiveEnergyBurned), quantity: caloriesQuantity, startDate: startDate, endDate: endDate) self.healthKitStore.addSamples([distanceSample,caloriesSample], toWorkout: workout, completion: { (success, error ) -> Void in completion(success, error) }) |
到现在,你可能对这些代码已经非常熟悉了,甚至可能到了你会认为你在做重复的事情的地步了。但是为了让这些更加清晰,这里做一下解释:
第 一步中,创建了两个quantity sample类型的对象,一个用DistanceWalkingRunning类型来代表跑步的距离。另一个用ActiveEnergyBurned类型 来代表消耗的卡路里,然后调用HealthKit Store的方法addsamples:ToWorkout将这两个数据添加到健康信息中。
编译并运行该应用,添加一到两条健康信息,然后关掉应用。既然这些健康信息已经和距离以及消耗的能量的数据关联起来了,你就可以在Health应用中看到它们了。
打开Health应用,前往Health Data栏,在这里选择Fitness选项,然后选择Walking+Running Distance或者Active Calories来检查一下数据是否保存到那里了。你将会看到类似如下界面:
太棒了!现在你已经有获得最重要的健康信息并将其保存到HealthKit Store中的能力了。
现在该干什么?
从这里你可以下载包含这篇教程中所有代码的工程。
! 重要!:如果你想使用上面的示例工程,在使用HealthKit之前需要进行一些设置,因为该工程绑定了一个示例用Bundle ID,你需要将其修改为你自己的Bundle ID,选择你的开发团队,然后将Target栏中Capabilities菜单下的HealthKit的开关由OFF变为ON。
详见上述“开始”部分和“授权与许可”部分。
但愿本篇教程能够就HealthKit的基础概念给你一些对认识与了解,让你明白怎么样在自己的应用中使用HealthKit。要了解更多关于HealthKit的相关知识,这里有一些相关资源:
-
2014年Apple的WWDC视频:关于HealthKit的App Store Review Guidelines,你应该确保你的应用遵守这些官方的Guidelines。
在 浏览过这些文档和视频之后,你应该准备好前往HealthKit更深入的方面,然后对本篇的这个应用做一些改善。例如,你可以添加一些新的类型的数据采样 信息或者是健康信息,使用HKStatisticsQuery计算统计结果,或者通过HKObserverQuery来观察Store中信息的改变。
我希望你能喜欢这篇教程,和以前一样,如果你有任何问题或者评论,请参与下方的讨论!
(本文为CocoaChina组织翻译,本译文权利归译者所有,未经允许禁止转载。)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
2014-09-24 Android中如何像 360 一样优雅的杀死后台服务而不启动
2014-09-24 Android下写一个永远不会被KILL掉的进程/服务
2014-09-24 android 程序防止被360或者系统给kill掉
2014-09-24 android如何让service不被杀死
2014-09-24 如何让自己的Android程序永不被系统kill
2013-09-24 java开源类库pinyin4j的使用