开发一个健身 App,用 HealthKit 来跟踪步行距离

原文链接
作者:AppCoda
原文日期:2016-03-22

看新闻我们也知道,比起历史上任何一个时刻,健身和健康在今天越来越重要了。说起了也挺好笑的,我似乎记得几天前新闻告诉我同样的事情,也许是因为年纪越来越大的缘故,更需要健康和健身了。不管怎么说,这是一个热门话题。随着技术的不断进步,手机应用和硬件在世界范围内都变得流行起来,这些都把日益流行的健身健康话题加入了新的元素。

HealthKit 是苹果公司的重要桥梁,把重要的跟踪的健康数据同有健康意识的科技消费者、运动迷、平常使用 iPhone 的人连接了起来。这很酷,用户可以很容易的就追踪衡量一段时间内的健身和健康数据,除了意识到的好处之外,我们看到图标中的向上走的曲线,就能给我们极大的鼓励,激励我们继续运动。

正如我们能想象到的,在管理健康信息时,数据安全成为非常重要的因素。HealthKit 对于所有的 HealthKit 信息有绝对的控制权,会直接传递到用户手中。用户可以准许或者拒绝任何 App 获取他们的健康数据的请求。

对于开发者来说,我们需要请求许可方能读取或者写入 HealthKit 数据。实际上,我们需要特别声明一下,我们想影响获取具体哪些数据。另外,任何使用 HealthKit 的 App 必须要包含一份 Privacy Policy(隐私协议),这样用户在进行信息交易时会觉得更舒服一些。

关于 OneHourWalker 走路一小时

今天,我们要创建一个非常有趣的 App,既能读取 HealthKit 中的信息,也能写入新的数据。看一下 OneHourWalker 的外表吧:

OneHourWalker 是健身 App,能够跟踪用户在一个小时内走路或跑步的距离。用户可以分享距离到 HealthKit上,就会被其他的健康 App 收集到数据。我知道,一小时听起来不太积极,至少对我而言是这样。最后,用户可以提早结束健身,仍然可以分享距离。

所以,听起来只需要把数据写入 HealthKit 即可。不过我们要读取的数据是什么?

好问题!当我走路时,我有可能是在羊肠小道上步行,也可能在森林中漫步。我常常穿越一些 low-hanging branches 区域。Being as I am 6’4”,这会带来一些问题。我们的解决方案是:我们会从 HealthKit 中读取用户的高度,然后显示到 Label 控件上。这样会比较友好地提示用户的限制,这样他或她就能避免在走路时纠缠在一起。

为了方便开始,这里有一个 OneHourWalker 的初始工程,下载然后运行,看起来好像 App 可以运行。计时器和定位系统都已经在运行了,所以我们只需要将注意力放在实施 HealthKit上,注意一下,六十分钟后,计时器和定位系统就会自动停止。

开启 HealthKit

第一步就是在应用中开启 HealthKit 功能,在 Project Navigator 中,选中 OneHourWalker,然后点击 Targets 下方的 OneHourWalker。接着,在屏幕上方的 tab 栏中点击 Capabilities。

找到 Capabilities 清单底下,把 HealthKit 调到 On 状态。这个动作表示:把 HealthKit 资格添加到 App ID 中,把 HealthKit key 添加到 info plist 文件里,把 HealthKit 资格添加到资格文件中,以及连接 HealthKit.framework。就是这么简单。

开始写代码吧

找到 TimerViewController.swift,开始把 HealthKit 引入 OneHourWalker。首先,让我们创建一个 HealthKitManager 实例。

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()

HealthKitManager.swift 里包含了所有有关 HealthKit 的操作。里面有一些重要的方法,我们马上就要开始在这个文件里进行编程了。

正如我们在开头介绍的那样,我们需要获取用户的许可来读取和写入他们的健康数据。在 ViewDidLoad(),开始获取许可吧:

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.requestWhenInUseAuthorization();
        
        if CLLocationManager.locationServicesEnabled(){
            locationManager.delegate = self
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
        }
        else {
            print("Need to Enable Location");
        }
        
        // We cannot access the user's HealthKit data without specific permission.
        getHealthKitPermission()
    }

getHealthKitPermission() 方法会调用 manager 的 authorizeHealthKit() 方法。如果一切顺利,我们可以调用 setHeight() 方法,不过很快我们就会需要更多方法了。

    func getHealthKitPermission() {
        
        // Seek authorization in HealthKitManager.swift.
        healthManager.authorizeHealthKit { (authorized,  error) -> Void in
            if authorized {
                
                // Get and set the user's height.
                self.setHeight()
            } else {
                if error != nil {
                    print(error)
                }
                print("Permission denied.")
            }
        }
    }

HealthKitManager.swift 文件里,我们创建 authorizeHealthKit() 方法。除此之外,我们还需要创建 HealthKit store,将 App 连接到 HealthKit 数据。

    let healthKitStore: HKHealthStore = HKHealthStore()
    
    func authorizeHealthKit(completion: ((success: Bool, error: NSError!) -> Void)!) {
        
        // State the health data type(s) we want to read from HealthKit.
        let healthDataToRead = Set(arrayLiteral: HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)!)
        
        // State the health data type(s) we want to write from HealthKit.
        let healthDataToWrite = Set(arrayLiteral: HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)!)
        
        // Just in case OneHourWalker makes its way to an iPad...
        if !HKHealthStore.isHealthDataAvailable() {
            print("Can't access HealthKit.")
        }
        
        // Request authorization to read and/or write the specific data.
        healthKitStore.requestAuthorizationToShareTypes(healthDataToWrite, readTypes: healthDataToRead) { (success, error) -> Void in
            if( completion != nil ) {
                completion(success:success, error:error)
            }
        }
    }


当我们请求授权获取用户健康数据时,我们需要特别表明我们只是想读取和写入数据。对于我们来说,我们想读取用户的高度,所以他们可以避免容易实现误导动作。希望 HealthKit 能够提供一个 HKObject 参数,可以获取一个可理解的高度数据。我们也需要请求许可方能写入 HKObject 参数,获取用户的走路或者跑步距离。

在处理了 OneHourWalker 的所有可能性后,我们在一个 iPad 真机上测试了一下,制作正式的的请求。

HealthKitManager.swift 文件中创建 getHeight() 方法,能够读取用户的高度数据。

    func getHeight(sampleType: HKSampleType , completion: ((HKSample!, NSError!) -> Void)!) {
        
        // Predicate for the height query
        let distantPastHeight = NSDate.distantPast() as NSDate
        let currentDate = NSDate()
        let lastHeightPredicate = HKQuery.predicateForSamplesWithStartDate(distantPastHeight, endDate: currentDate, options: .None)
        
        // Get the single most recent height
        let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
        
        // Query HealthKit for the last Height entry.
        let heightQuery = HKSampleQuery(sampleType: sampleType, predicate: lastHeightPredicate, limit: 1, sortDescriptors: [sortDescriptor]) { (sampleQuery, results, error ) -> Void in
                
                if let queryError = error {
                    completion(nil, queryError)
                    return
                }
                
                // Set the first HKQuantitySample in results as the most recent height.
                let lastHeight = results!.first
            
                if completion != nil {
                    completion(lastHeight, nil)
                }
        }
        
        // Time to execute the query.
        self.healthKitStore.executeQuery(heightQuery)
    }
    

我们第一步就是查询高度数据来创建一个猜测的时间参数,我们获取一段时间内的所有高度信息,从过去到现在,当然了,这就会返回给我们一个数组。我们只想要最近的高度,所以我们会让数据中最新的数据排在最前面。

在创建查询的过程中,我们会限制数组的总数为一。把可能出错的情况计算在内,我们把第一个也是唯一一个 item 作为 lastHeight 的结果。接着,我们搞定 getHeight() 方法。最后,执行查询对应用户的健康数据。

回到 TimerViewController.swift,在 App 出现在用户之前,我们假设已经获取了用户的准许,setHeight()getHealthKitPermission() 被调用。

var height: HKQuantitySample?

首先,我们需要声明一个高度变量作为 HKQuantitySample 的实例。

    func setHeight() {
        // Create the HKSample for Height.
        let heightSample = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)
        
        // Call HealthKitManager's getSample() method to get the user's height.
        self.healthManager.getHeight(heightSample!, completion: { (userHeight, error) -> Void in
            
            if( error != nil ) {
                print("Error: \(error.localizedDescription)")
                return
            }
            
            var heightString = ""
            
            self.height = userHeight as? HKQuantitySample
            
            // The height is formatted to the user's locale.
            if let meters = self.height?.quantity.doubleValueForUnit(HKUnit.meterUnit()) {
                let formatHeight = NSLengthFormatter()
                formatHeight.forPersonHeightUse = true
                heightString = formatHeight.stringFromMeters(meters)
            }
            
            // Set the label to reflect the user's height.
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                self.heightLabel.text = heightString
            })
        })
        
    }

上面的 share() 方法,我们会创建我们的 setHeigth() 方法。高度数据例子表明我们请求之后返回的一个 HKQuantity,它的 identifier 也就是 HKQuantityTypeIdentifierHeight

下一步,我们调用 getHeight() 方法,也就是我们在 manager 中创建的。有了高度,我们需要将它转换成合适的字符串,展示到我们的 Label 控件中。照例,我们要考虑所有可能的错误。

就这点而言,用户能够打开 App,看一下他们的高度,假设这里有记录高度的 App,开始计时器,然后追踪跑步或者走路的距离。下一步就是处理写入数据,所以用户可以记录所有的健身数据。

60分钟后或更短时间内用户完成运动,他/她会点击 Share 按钮将他们的距离发送到 Health 应用里。所以,在 share() 方法中,让我们调用 HealthKitManager.swift 里的 saveDistance() 方法,这样,数据和日期都能被归档,用户可以明天试着去挑战他/她自己的记录!

    @IBAction func share(sender: AnyObject) {
        healthManager.saveDistance(distanceTraveled, date: NSDate())
    }

回到 manager,我们创建 saveDistance() 方法,首先,我们需要让 HealthKit 知道我们想写入跑步距离和走路步数,接着,我们将计量单位换成英里,分配正式的数量。HealthKit 的 saveObject() 方法将会写入用户的健康数据。

    
    func saveDistance(distanceRecorded: Double, date: NSDate ) {
                
        // Set the quantity type to the running/walking distance.
        let distanceType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
        
        // Set the unit of measurement to miles.
        let distanceQuantity = HKQuantity(unit: HKUnit.mileUnit(), doubleValue: distanceRecorded)
        
        // Set the official Quantity Sample.
        let distance = HKQuantitySample(type: distanceType!, quantity: distanceQuantity, startDate: date, endDate: date)
        
        // Save the distance quantity sample to the HealthKit Store.
        healthKitStore.saveObject(distance, withCompletion: { (success, error) -> Void in
            if( error != nil ) {
                print(error)
            } else {
                print("The distance has been recorded! Better go check!")
            }
        })
    }

回到 Health 应用里,记录的数据会包含在 Walking + Running Distance 里。当然,我们也能看到一个具体的例子:Health Data tab > Fitness > Walking + Running Distance > Show All Data。我们的数据就在这清单里。点击一行,然后就会看到我们的图标(目前还空着)。再次点击这一行,就会出现所有的详细信息。

有了 OneHourWalker,我们已经成功地为全世界的 iOS 用户的健康贡献了我们的力量。然而,这仅仅是一个开始。仍然有更多利用 HealthKit 读取和写入健康数据的事情需要我们来做。

当然,能够获取用户的所有跟踪信息也是非常棒的,人们可以非常容易的进行每天和每天的比较,周和周的对比,以及其他朝着目标推进的对比。真正伟大之处在于,开发者能够提供全新的、有创造力的、有趣的方式来捕获这些数据。

同样的,HealthKit 应用是最有趣的测试了!

这里是我们最终版本的 OneHourWalker

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 作者:AppCoda,原文链接,原文日期:2016-03-22译者:Crystal Sun;校对:numbbbbb...
    梁杰_numbbbbb阅读 899评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 简介 App 正在改变世界,丰富人们的生活,并为像您一样的开发者提供前所未有的创新机会。因此,App Store ...
    o0_0o阅读 3,192评论 2 48
  • 一场雷阵雨,家里也是小暴雨。不要问我家里怎么回事,不想多谈。老天爷生的我就是该受这些罪,我有什么办法。 今天,和家...
    Gump灬咼阅读 204评论 0 0