用 HealthKit 来开发一个iOS健身 App

麒¥麟 9年前
   <p>看新闻我们也知道,比起历史上任何一个时刻,健身和健康在今天都更加重要。说起来也挺好笑的,我似乎记得几天前新闻也在说同样的事情,也许是因为年纪越来越大的缘故,我更需要健康和健身。不管怎么说,这是一个热门话题。随着技术的不断进步,手机应用和硬件在世界范围内都变得流行起来,这些都给日益流行的健身健康话题加入了新的元素。</p>    <p>HealthKit 是苹果公司的重要桥梁,把追踪的重要的健康数据同有健康意识的科技消费者、运动迷、平常使用 iPhone 的人连接了起来。这很酷,用户可以很容易的就追踪衡量一段时间内的健身和健康数据,除了意识到的好处之外,我们看到图标中向上走的曲线,就能给我们极大的鼓励,激励我们继续运动。</p>    <p>正如我们能想象到的,在管理健康信息时,数据安全成为非常重要的因素。HealthKit 对于所有的 HealthKit 信息有绝对的控制权,会直接传递到用户手中。用户可以准许或者拒绝任何 App 获取他们的健康数据的请求。</p>    <p>对于开发者来说,我们需要请求许可方能读取或者写入 HealthKit 数据。实际上,我们需要特别声明一下,我们想影响获取具体哪些数据。另外,任何使用 HealthKit 的 App 必须要包含一份 Privacy Policy(隐私协议),这样用户在进行信息交易时会觉得更舒服一些。</p>    <h3>关于走路一小时(OneHourWalker)</h3>    <p>今天,我们要创建一个非常有趣的 App,既能读取 HealthKit 中的信息,也能写入新的数据。看一下 OneHourWalker 的外表吧:</p>    <p><img src="https://simg.open-open.com/show/79264641d66c2c7353a4bd31bf0c4be1.png"></p>    <p>OneHourWalker 是一个健身 App,能够跟踪用户在一个小时内走路或跑步的距离。用户可以把距离分享到 HealthKit,这样就能在健康应用中查看。我知道,整整一个小时听起来确实有点吓人,至少对我而言是这样。因此,用户可以提前结束健身,此时仍然可以分享距离。</p>    <p>所以,听起来只需要把数据写入 HealthKit 即可。不过我们要读取的数据是什么?</p>    <p>好问题!我喜欢在树林里的小路上漫步。我常常穿越一些枝杈纵横的区域。因为我是八尺大汉,这会带来一些问题。我们的解决方案是:我们会从 HealthKit 中读取用户的身高,然后显示到 Label 控件上。这样会比较友好地提示用户,帮他避免不适合运动的区域。</p>    <p>下面是 OneHourWalker 的 <a href="/misc/goto?guid=4959673098614509241" rel="nofollow,noindex">初始工程</a> ,下载然后运行,看起来好像 App 可以运行。计时器和定位系统都已经在运行了,所以我们只需要将注意力放在使用 HealthKit 上,注意一下,六十分钟后,计时器和定位系统就会自动停止。</p>    <h3>启用 HealthKit</h3>    <p>第一步就是在应用中开启 HealthKit 功能,在 Project Navigator 中,选中 OneHourWalker,然后点击 Targets 下方的 OneHourWalker。接着,在屏幕上方的 tab 栏中点击 Capabilities。</p>    <p><img src="https://simg.open-open.com/show/0a186409074888d5e0bc6520f3c8fee8.png"></p>    <p>在 Capabilities 底部把 HealthKit 设置为 On。这会把 HealthKit entitlement 添加到 App ID 中、把 HealthKit key 添加到 info plist 文件中、把 HealthKit entitlement 添加到资格文件中、连接 HealthKit.framework 。就是这么简单。</p>    <h3>开始写代码吧</h3>    <p>找到 TimerViewController.swift ,下面我们给 OneHourWalker 添加 HealthKit。首先我们创建一个 HealthKitManager 实例。</p>    <pre>  <code class="language-objectivec">import UIKit  import CoreLocation  import HealthKit    class TimerViewController: UIViewController, CLLocationManagerDelegate {        @IBOutlet weak var timerLabel: UILabel!      @IBOutlet weak var milesLabel: UILabel!      @IBOutlet weak var heightLabel: UILabel!            var zeroTime = NSTimeInterval()      var timer : NSTimer = NSTimer()            let locationManager = CLLocationManager()      var startLocation: CLLocation!      var lastLocation: CLLocation!      var distanceTraveled = 0.0            let healthManager:HealthKitManager = HealthKitManager()  </code></pre>    <p>HealthKitManager.swift 里包含了所有和 HealthKit 有关的操作。里面有一些重要的方法,稍后我们会实现它。</p>    <p>正如开头介绍的那样,我们需要获取用户的授权,从而读取和写入他们的健康数据。在 ViewDidLoad() 中获取授权:</p>    <pre>  <code class="language-objectivec">override func viewDidLoad() {      super.viewDidLoad()        locationManager.requestWhenInUseAuthorization();            if CLLocationManager.locationServicesEnabled(){          locationManager.delegate = self          locationManager.desiredAccuracy = kCLLocationAccuracyBest      }      else {          print("Need to Enable Location");      }            // 不向用户请求许可就无法获取用户的 HealthKit 数据      getHealthKitPermission()  }  </code></pre>    <p>getHealthKitPermission() 方法会调用 manager 的 authorizeHealthKit() 方法。如果一切顺利,我们可以调用 setHeight() 方法,稍后我们会介绍这个方法。</p>    <pre>  <code class="language-objectivec">func getHealthKitPermission() {      // 在 HealthKitManager.swift 文件里寻找授权情况。      healthManager.authorizeHealthKit { (authorized,  error) -> Void in          if authorized {                            // 获得然后设置用户的高度              self.setHeight()          } else {              if error != nil {                  print(error)              }              print("Permission denied.")          }      }  }  </code></pre>    <p>在 HealthKitManager.swift 文件中创建 authorizeHealthKit() 方法。除此之外,我们还需要创建 HealthKit store,将 App 连接到 HealthKit 数据。</p>    <pre>  <code class="language-objectivec">let healthKitStore: HKHealthStore = HKHealthStore()    func authorizeHealthKit(completion: ((success: Bool, error: NSError!) -> Void)!) {            // 声明我们想从 HealthKit 里读取的健康数据的类型      let healthDataToRead = Set(arrayLiteral: HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)!)            // 声明我们想写入 HealthKit 的数据的类型      let healthDataToWrite = Set(arrayLiteral: HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!)            // 以防万一 OneHourWalker 在 iPad 中打开      if !HKHealthStore.isHealthDataAvailable() {          print("Can't access HealthKit.")      }            // 请求可以读取和写入数据的权限      healthKitStore.requestAuthorizationToShareTypes(healthDataToWrite, readTypes: healthDataToRead) { (success, error) -> Void in          if( completion != nil ) {              completion(success:success, error:error)          }      }  }  </code></pre>    <p>当我们请求授权获取用户健康数据时,需要特别表明我们只是想读取和写入数据。对于这个应用来说,我们想读取用户的身高,从而帮助他们避免撞到树枝。我们期望 HealthKit 提供一个 HKObject 实体,我们可以把它转换成可读性更高的身高值。此外,我们还需要申请写入权限,从而把用户步行和跑步的距离写入 HKObject 实体。</p>    <p>我们会在处理完 iPad 屏幕适配之后发起权限请求。</p>    <p>我们在 HealthKitManager.swift 文件中创建 getHeight() 方法,从 HealthKit 中读取用户的高度数据。</p>    <pre>  <code class="language-objectivec">func getHeight(sampleType: HKSampleType , completion: ((HKSample!, NSError!) -> Void)!) {            // 创建断言,以查询高度      let distantPastHeight = NSDate.distantPast() as NSDate      let currentDate = NSDate()      let lastHeightPredicate = HKQuery.predicateForSamplesWithStartDate(distantPastHeight, endDate: currentDate, options: .None)            // 获得最近的高度值      let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)        // 从 HealthKit 里获取最近的高度值      let heightQuery = HKSampleQuery(sampleType: sampleType, predicate: lastHeightPredicate, limit: 1, sortDescriptors: [sortDescriptor]) { (sampleQuery, results, error ) -> Void in                            if let queryError = error {                  completion(nil, queryError)                  return              }                // 把第一个 HKQuantitySample 作为最近的高度值              let lastHeight = results!.first                        if completion != nil {                  completion(lastHeight, nil)              }      }            // 是时候执行查询了      self.healthKitStore.executeQuery(heightQuery)  }  </code></pre>    <p>查询身高数据的第一步是创建一个断言,用它定义时间参数。我们会获取一段时间内的所有身高信息。当然,这会返回一个数组。我们只想要最近的身高,所以我们对数据排序,让数据中最新的数据排在最前面。</p>    <p>在创建查询的过程中,我们把数组的长度限制为一。处理完可能出现的错误之后,我们把第一个也是唯一一个 item 作为 lastHeight 的结果。接着,调用 getHeight() 的回调函数。最后,执行我们的查询操作。</p>    <p>回到 TimerViewController.swift ,在用户授权完成之后,用户开始使用 App 之前,需要在 getHealthKitPermission() 中调用 setHeight() 。</p>    <pre>  <code class="language-objectivec">var height: HKQuantitySample?  </code></pre>    <p>首先,我们需要给 HKQuantitySample 实例声明一个高度变量。</p>    <pre>  <code class="language-objectivec">func setHeight() {        // 创建高度 HKSample。      let heightSample = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)            // 调用 HealthKitManager 的 getSample() 方法,来获取用户的高度。      self.healthManager.getHeight(heightSample!, completion: { (userHeight, error) -> Void in                    if( error != nil ) {              print("Error: \(error.localizedDescription)")              return          }                    var heightString = ""                    self.height = userHeight as? HKQuantitySample                    // 把高度转换成用户本地的计量单位。          if let meters = self.height?.quantity.doubleValueForUnit(HKUnit.meterUnit()) {              let formatHeight = NSLengthFormatter()              formatHeight.forPersonHeightUse = true              heightString = formatHeight.stringFromMeters(meters)          }                    // 设置 label 显示用户的高度。          dispatch_async(dispatch_get_main_queue(), { () -> Void in              self.heightLabel.text = heightString          })      })        }  </code></pre>    <p>在 share() 方法之前创建 setHeigth() 方法。我们请求的身高数据会返回一个 HKQuantity ,它的 identifier 是 HKQuantityTypeIdentifierHeight 。</p>    <p>接着,我们调用 manager 中的 getHeight() 方法。有了身高数据,我们需要将它转换成合适的字符串,展示到我们的 Label 控件中。照例,我们要考虑所有可能的错误。</p>    <p>现在,用户能够打开 App,查看他们的身高,将身高记录到健康应用中,开始计时,然后追踪跑步或者走路的距离。下一步就是处理写入数据,让用户可以记录所有的健身数据。</p>    <p>用户完成运动之后(无论是整整一小时还是不到一小时),他/她会点击 Share 按钮,将他们的距离发送给 Health 应用。所以我们在 share() 方法中调用 HealthKitManager.swift 里的 saveDistance() 方法,这样数据和日期都能被归档,明天用户可以试着去挑战他/她自己的记录!</p>    <pre>  <code class="language-objectivec">@IBAction func share(sender: AnyObject) {      healthManager.saveDistance(distanceTraveled, date: NSDate())  }  </code></pre>    <p>回到 manager,我们创建 saveDistance() 方法,首先,我们需要让 HealthKit 知道我们想写入跑步距离和走路步数,接着,我们将计量单位换成英里并赋值给实体。HealthKit 的 saveObject() 方法将会写入用户的健康数据。</p>    <pre>  <code class="language-objectivec">func saveDistance(distanceRecorded: Double, date: NSDate ) {                    // 设置跑步距离或走路步数的数量的类型      let distanceType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)            // 把计量单位设置成英里      let distanceQuantity = HKQuantity(unit: HKUnit.mileUnit(), doubleValue: distanceRecorded)        // 设置正式的 Quantity Sample。      let distance = HKQuantitySample(type: distanceType!, quantity: distanceQuantity, startDate: date, endDate: date)            // 保存距离数量,把健康数据写入 HealthKit      healthKitStore.saveObject(distance, withCompletion: { (success, error) -> Void in          if( error != nil ) {              print(error)          } else {              print("The distance has been recorded! Better go check!")          }      })  }  </code></pre>    <p>打开健康应用,记录的数据会包含在 Walking + Running Distance 里。我们可以查看具体的记录:Health Data tab > Fitness > Walking + Running Distance > Show All Data。我们的数据就在这清单里。点击任意一行就会看到我们的图标(目前还空着)。再次点击这一行,就会出现所有的详细信息。</p>    <p><img src="https://simg.open-open.com/show/12a8a6af36a83309e803d4d1c9b4d9a6.png"></p>    <p>有了 OneHourWalker,我们就可以为全世界 iOS 用户的健康贡献我们的力量。然而,这仅仅是一个开始。HealthKit 有无限可能。</p>    <p>当然,让用户查看所有追踪信息非常有用,人们可以对比每天、每周或者任意时间的数据,从而给自己动力。但是真正有价值的是,开发者可以用无数种新的、有创造力的、有趣的方式来获取数据。</p>    <p>此外,HealthKit 应用的测试会非常有趣!</p>    <p>这里是我们最终版本的 <a href="/misc/goto?guid=4959673098704140164" rel="nofollow,noindex">OneHourWalker</a> 。</p>    <p>via: http://swift.gg/2016/05/13/healthkit-introduction/</p>