Xamarin.iOS 中的 HealthKit
运行状况工具包可以安全地存储用户的运行状况相关信息数据。 得到用户明确许可后,运行状况工具包应用可以读取和写入此数据存储,并在添加相关数据时接收通知。 应用可以显示数据,或者用户可以使用 Apple 提供的运行状况应用查看其所有数据的仪表板。
由于与健康相关的数据非常敏感且至关重要,因此 Health Kit 是强类型化的,具有度量单位以及与记录的信息类型(例如血糖水平或心率)的显式关联。 此外,Health Kit 应用必须使用显式权利,必须请求访问特定类型的信息,并且用户必须显式授予应用访问此类数据的权限。
本文将介绍:
- Health Kit 的安全要求,包括应用程序预配和请求对 Health Kit 数据库的用户访问权限;
- Health Kit 的类型系统,该系统可以最大程度地减少错误应用或错误解释数据的可能性;
- 写入到系统范围的共享 Health Kit 数据存储。
本文不会介绍更高级的主题,例如查询数据库、度量单位之间的转换或接收新数据的通知。
在本文中,我们将创建一个示例应用程序来记录用户的心率:
要求
要完成本文所述的步骤,需要满足以下条件:
- Xcode 7 和 iOS 8(或更高版本)– Apple 的最新 Xcode 和 iOS API 需要在开发者的计算机上安装和配置。
- Visual Studio for Mac 或 Visual Studio - 应在开发人员计算机上安装并配置最新版本的 Visual Studio for Mac。
- iOS 8(或更高版本)设备 – 运行最新版 iOS 8 或更高版本的测试 iOS 设备。
重要
Health Kit 已在 iOS 8 中引入。 目前,Health Kit 在 iOS 模拟器上不可用,调试需要连接到物理 iOS 设备。
创建并配置 Health Kit 应用
在 Xamarin iOS 8 应用程序可以使用 HealthKit API 之前,必须对其进行正确配置和预配。 本部分将介绍正确设置 Xamarin 应用程序所需的步骤。
Health Kit 应用需要:
- 显式的应用 ID。
- 与该显式应用 ID 和 Health Kit 权限关联的预配配置文件。
- 一个
Entitlements.plist
,其Boolean
类型的com.apple.developer.healthkit
属性设置为Yes
。 - 一个
Info.plist
,其UIRequiredDeviceCapabilities
键包含String
值为healthkit
的条目。 Info.plist
还必须具有相应的隐私解释条目:如果应用要写入数据,则为键NSHealthUpdateUsageDescription
提供String
解释;如果应用要读取 Health Kit 数据,则为键NSHealthShareUsageDescription
提供String
解释。
若要详细了解如何预配 iOS 应用,请参阅 Xamarin“入门”系列教程中的设备预配一文,其中介绍了开发人员证书、应用 ID、预配配置文件和应用权利之间的关系。
显式应用 ID 和预配配置文件
显式应用 ID 和相应 预配配置文件的创建是在 Apple 的 iOS 开发人员中心完成的。
你的当前应用 ID 列在开发人员中心的“证书、标识符和配置文件”部分。 通常,此列表会显示 *
的“ID”值,表示“应用 ID” - “名称”可与任意数量的后缀结合使用。 此类通配符应用 ID 不能与 Health Kit 结合使用。
若要创建显式应用 ID,请单击右上角的 + 按钮转到“注册 iOS 应用 ID”页:
如上图所示,创建应用说明后,使用显式“应用 ID 部分”为应用程序创建 ID。 在“应用服务”部分,选中“启用服务”部分中的“Health Kit”。
完成后,按“继续”按钮在帐户中注册应用 ID。 你将返回到“证书、标识符和配置文件”页。 单击“预配配置文件”转到当前预配配置文件的列表,然后单击右上角的 + 按钮转到“添加 iOS 预配配置文件”页。 选择“iOS 应用开发”选项,然后单击“继续”转到“选择应用 ID”页。 在此处,请选择之前指定的显式应用 ID:
单击“继续”并完成其余屏幕中的设置,需在其中指定此预配配置文件的“开发人员证书”、“设备”和“名称”:
单击“生成”并等待创建配置文件。 下载该文件,然后双击以在 Xcode 中安装。 可以在“Xcode”“首选项”“帐户”“查看详细信息...”下确认其安装状态。你应会看到刚刚安装的预配配置文件,并且其“Entitlements”行中应包含 Health Kit 和任何其他特殊服务的图标>>>:
将应用 ID 和预配配置文件与 Xamarin.iOS 应用相关联
如前所述创建并安装相应的预配配置文件后,通常需要在 Visual Studio for Mac 或 Visual Studio 中创建解决方案。 Health Kit 访问权限可用于任何 iOS C# 或 F# 项目。
无需手动完成创建 Xamarin iOS 8 项目的过程,可以打开本文附带的示例应用(其中包括预生成的情节提要和代码)。 若要将示例应用与已启用 Health Kit 的预配配置文件相关联,请在“Solution Pad”中右键单击你的项目并打开其“选项”对话框。 切换到“iOS 应用程序”面板,然后输入之前创建的显式应用 ID,作为应用的捆绑标识符:
现在切换到“iOS 捆绑签名”面板。 最近安装的预配配置文件及其与显式应用 ID 的关联现在将作为预配配置文件提供:
如果预配配置文件不可用,请仔细检查“iOS 应用程序”面板中的“捆绑标识符”与“iOS 开发人员中心”中指定的标识符是否有差别,以及是否已安装预配配置文件(“Xcode”>“首选项”>“帐户”>“查看详细信息...”)。
选择已启用 Health Kit 的预配配置文件后,单击“确定”关闭“项目选项”对话框。
Entitlements.plist 和 Info.plist 值
示例应用包含一个 Entitlements.plist
文件(对于已启用 Health Kit 的应用是必需的),但并未包含在每个项目模板中。 如果项目不包含权利,请右键单击项目,选择“文件”>“新建文件...”>“iOS”>“Entitlements.plist”以手动添加一个权利。
最终,Entitlements.plist
必须包含以下键值对:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.HealthKit</key>
<true/>
</dict>
</plist>
同样,应用的 Info.plist
必须包含与 UIRequiredDeviceCapabilities
键关联的 healthkit
值:
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>healthkit</string>
</array>
本文提供的示例应用程序包含一个预配置的 Entitlements.plist
,其中包含所有必需的键。
Health Kit 编程
Health Kit 数据存储是一个专用的、特定于用户的数据存储,在应用之间共享。 由于健康信息非常敏感,用户必须采取积极的措施来允许数据访问。 这种访问权限可能是部分性的(写入但不读取、访问某些类型的数据但不访问其他类型的数据等),并且随时可以撤销。 应以防御的方式编写 Health Kit 应用程序,因为许多用户对于存储其健康相关的信息会感到犹豫不决。
Health Kit 数据仅限于 Apple 指定的类型。 这些类型是严格定义的:有些类型(例如血型)仅限于 Apple 提供的枚举的特定值,而有些类型则将大小与度量单位(例如克、卡路里和升)结合起来。 即使共享兼容度量单位的数据也会按其 HKObjectType
进行区分;例如,类型系统会捕获将 HKQuantityTypeIdentifier.NumberOfTimesFallen
值存储到需要 HKQuantityTypeIdentifier.FlightsClimbed
的字段的错误尝试,即使两者都使用 HKUnit.Count
度量单位。
Health Kit 数据存储中可存储的类型都是 HKObjectType
的子类。 HKCharacteristicType
对象存储生物性别、血型和出生日期。 但更常见的是 HKSampleType
对象,它们表示在特定时间或一段时间内采样的数据。
HKSampleType
是抽象的,有四个具体的子类。 目前只有一种类型的 HKCategoryType
数据,即睡眠分析。 Health Kit 中的绝大部分数据都是 HKQuantityType
类型,其数据存储在 HKQuantitySample
对象中,这些对象是使用熟悉的工厂设计模式创建的:
HKQuantityType
类型范围为 HKQuantityTypeIdentifier.ActiveEnergyBurned
到 HKQuantityTypeIdentifier.StepCount
。
从用户请求权限
最终用户必须采取积极措施来允许应用读取或写入 Health Kit 数据。 这是通过 iOS 8 设备上预装的“健康”应用完成的。 首次运行 Health Kit 应用时,系统会向用户显示一个由系统控制的“健康数据访问”对话框:
稍后,用户可以使用“健康”应用的“源”对话框更改权限:
由于健康信息极其敏感,应用开发人员应该以防御方式编写程序,并预期在应用运行时权限会被拒绝和更改。 最常见的惯例是在 UIApplicationDelegate.OnActivated
方法中请求权限,然后相应地修改用户界面。
权限演练
在 Health Kit 预配的项目中,打开 AppDelegate.cs
文件。 请注意文件顶部使用 HealthKit
的语句。
以下代码与 Health Kit 权限相关:
private HKHealthStore healthKitStore = new HKHealthStore ();
public override void OnActivated (UIApplication application)
{
base.OnActivated(application);
ValidateAuthorization ();
}
private void ValidateAuthorization ()
{
var heartRateId = HKQuantityTypeIdentifierKey.HeartRate;
var heartRateType = HKObjectType.GetQuantityType (heartRateId);
var typesToWrite = new NSSet (new [] { heartRateType });
var typesToRead = new NSSet ();
healthKitStore.RequestAuthorizationToShare (
typesToWrite,
typesToRead,
ReactToHealthCarePermissions);
}
void ReactToHealthCarePermissions (bool success, NSError error)
{
var access = healthKitStore.GetAuthorizationStatus (HKObjectType.GetQuantityType (HKQuantityTypeIdentifierKey.HeartRate));
if (access.HasFlag (HKAuthorizationStatus.SharingAuthorized)) {
HeartRateModel.Instance.Enabled = true;
} else {
HeartRateModel.Instance.Enabled = false;
}
}
这些方法中的所有代码都可以在 OnActivated
中内联完成,但示例应用使用单独的方法来使它们的意图更清晰:ValidateAuthorization()
包含请求访问正在写入的特定类型(以及根据应用的需要进行读取)所需的步骤,而 ReactToHealthCarePermissions()
是用户与 Health.app 中的权限对话框交互后激活的回调。
ValidateAuthorization()
的任务是生成应用将写入的 HKObjectTypes
集,并请求授权更新该数据。 在示例应用中,HKObjectType
用于键 KHQuantityTypeIdentifierKey.HeartRate
。 此类型将添加到 typesToWrite
集,而 typesToRead
集保留为空。 这些集以及对 ReactToHealthCarePermissions()
回调的引用将传递给 HKHealthStore.RequestAuthorizationToShare()
。
在用户与权限对话框交互并传递以下两条信息后,将调用 ReactToHealthCarePermissions()
回调:一个 bool
值,如果用户与权限对话框交互,则该值为 true
;一个 NSError
值,如果不为 null,则表示与显示权限对话框相关的某种错误。
重要
此函数的参数的澄清:success 和 error 参数并不指示用户是否已授予访问 Health Kit 数据的权限! 它们仅表明用户有机会允许访问数据。
若要确认应用是否有权访问数据,请使用 HKHealthStore.GetAuthorizationStatus()
并传入 HKQuantityTypeIdentifierKey.HeartRate
。 根据返回的状态,应用将启用或禁用输入数据的功能。 没有标准的用户体验用于处理拒绝访问错误,有许多可能的选项。 在示例应用中,状态是在 HeartRateModel
单一实例对象上设置的,而该对象又会引发相关事件。
模型、视图和控制器
若要查看 HeartRateModel
单一实例对象,请打开 HeartRateModel.cs
文件:
using System;
using HealthKit;
using Foundation;
namespace HKWork
{
public class GenericEventArgs<T> : EventArgs
{
public T Value { get; protected set; }
public DateTime Time { get; protected set; }
public GenericEventArgs (T value)
{
this.Value = value;
Time = DateTime.Now;
}
}
public delegate void GenericEventHandler<T> (object sender,GenericEventArgs<T> args);
public sealed class HeartRateModel : NSObject
{
private static volatile HeartRateModel singleton;
private static object syncRoot = new Object ();
private HeartRateModel ()
{
}
public static HeartRateModel Instance {
get {
//Double-check lazy initialization
if (singleton == null) {
lock (syncRoot) {
if (singleton == null) {
singleton = new HeartRateModel ();
}
}
}
return singleton;
}
}
private bool enabled = false;
public event GenericEventHandler<bool> EnabledChanged;
public event GenericEventHandler<String> ErrorMessageChanged;
public event GenericEventHandler<Double> HeartRateStored;
public bool Enabled {
get { return enabled; }
set {
if (enabled != value) {
enabled = value;
InvokeOnMainThread(() => EnabledChanged (this, new GenericEventArgs<bool>(value)));
}
}
}
public void PermissionsError(string msg)
{
Enabled = false;
InvokeOnMainThread(() => ErrorMessageChanged (this, new GenericEventArgs<string>(msg)));
}
//Converts its argument into a strongly-typed quantity representing the value in beats-per-minute
public HKQuantity HeartRateInBeatsPerMinute(ushort beatsPerMinute)
{
var heartRateUnitType = HKUnit.Count.UnitDividedBy (HKUnit.Minute);
var quantity = HKQuantity.FromQuantity (heartRateUnitType, beatsPerMinute);
return quantity;
}
public void StoreHeartRate(HKQuantity quantity)
{
var bpm = HKUnit.Count.UnitDividedBy (HKUnit.Minute);
//Confirm that the value passed in is of a valid type (can be converted to beats-per-minute)
if (! quantity.IsCompatible(bpm))
{
InvokeOnMainThread(() => ErrorMessageChanged(this, new GenericEventArgs<string> ("Units must be compatible with BPM")));
}
var heartRateId = HKQuantityTypeIdentifierKey.HeartRate;
var heartRateQuantityType = HKQuantityType.GetQuantityType (heartRateId);
var heartRateSample = HKQuantitySample.FromType (heartRateQuantityType, quantity, new NSDate (), new NSDate (), new HKMetadata());
using (var healthKitStore = new HKHealthStore ()) {
healthKitStore.SaveObject (heartRateSample, (success, error) => {
InvokeOnMainThread (() => {
if (success) {
HeartRateStored(this, new GenericEventArgs<Double>(quantity.GetDoubleValue(bpm)));
} else {
ErrorMessageChanged(this, new GenericEventArgs<string>("Save failed"));
}
if (error != null) {
//If there's some kind of error, disable
Enabled = false;
ErrorMessageChanged (this, new GenericEventArgs<string>(error.ToString()));
}
});
});
}
}
}
}
第一部分是用于创建通用事件和处理程序的样板代码。 HeartRateModel
类的初始部分也是用于创建线程安全单一实例对象的样板。
然后,HeartRateModel
公开 3 个事件:
EnabledChanged
- 指示心率数据存储已启用或禁用(请注意,存储最初已禁用)。ErrorMessageChanged
- 对于此示例应用,我们有一个非常简单的错误处理模型:包含最后一个错误的字符串。HeartRateStored
- 当心率数据存储在 Health Kit 数据库中时引发。
请注意,每当触发这些事件时,都是通过 NSObject.InvokeOnMainThread()
完成的,这样订阅者就可以更新 UI。 或者,可以将事件记录为在后台线程上引发,确保兼容性的责任可以留给它们的处理程序。 线程考虑因素在 Health Kit 应用程序中非常重要,因为许多功能(例如权限请求)都是异步的,并且在非主线程上执行其回调。
HeartRateModel
中 Heath Kit 特定的代码位于 HeartRateInBeatsPerMinute()
和 StoreHeartRate()
这两个函数中。
HeartRateInBeatsPerMinute()
将其参数转换为强类型化的 Health Kit HKQuantity
。 数量类型是由 HKQuantityTypeIdentifierKey.HeartRate
指定的类型,数量单位是 HKUnit.Count
除以 HKUnit.Minute
(换言之,单位是每分钟心跳次数)。
StoreHeartRate()
函数采用 HKQuantity
(在示例应用中,由 HeartRateInBeatsPerMinute()
创建)。 为了验证其数据,它使用 HKQuantity.IsCompatible()
方法,如果对象的单位可以转换为参数中的单位,则该方法返回 true
。 如果数量是使用 HeartRateInBeatsPerMinute()
创建的,则此方法显然会返回 true
,但如果数量创建为“每小时心跳次数”,则也会返回 true
。 更常见的是,HKQuantity.IsCompatible()
可用于验证用户或设备可能在一种度量系统(例如英制单位)中输入或显示,但存储在另一种系统(例如公制单位)中的质量、距离和能量。
验证数量的兼容性后,将使用 HKQuantitySample.FromType()
工厂方法创建强类型化的 heartRateSample
对象。 HKSample
对象具有开始日期和结束日期;对于瞬时读数,这些值应与示例中的值相同。 此外,该示例未在其 HKMetadata
参数中设置任何键值数据,但可以使用如下代码来指定传感器位置:
var hkm = new HKMetadata();
hkm.HeartRateSensorLocation = HKHeartRateSensorLocation.Chest;
创建 heartRateSample
后,代码将使用 using 块创建与数据库的新连接。 在该块中,HKHealthStore.SaveObject()
方法尝试异步写入数据库。 对 lambda 表达式的最终调用会触发相关事件:HeartRateStored
或 ErrorMessageChanged
。
对模型进行编程后,接下来可以查看控制器如何对模型的状态做出反应。 打开 HKWorkViewController.cs
文件。 构造函数只是将 HeartRateModel
单一实例连接到事件处理方法(同样,这可以通过 lambda 表达式内联完成,但单独的方法使意图更加明显):
public HKWorkViewController (IntPtr handle) : base (handle)
{
HeartRateModel.Instance.EnabledChanged += OnEnabledChanged;
HeartRateModel.Instance.ErrorMessageChanged += OnErrorMessageChanged;
HeartRateModel.Instance.HeartRateStored += OnHeartBeatStored;
}
下面是相关处理程序:
void OnEnabledChanged (object sender, GenericEventArgs<bool> args)
{
StoreData.Enabled = args.Value;
PermissionsLabel.Text = args.Value ? "Ready to record" : "Not authorized to store data.";
PermissionsLabel.SizeToFit ();
}
void OnErrorMessageChanged (object sender, GenericEventArgs<string> args)
{
PermissionsLabel.Text = args.Value;
}
void OnHeartBeatStored (object sender, GenericEventArgs<double> args)
{
PermissionsLabel.Text = String.Format ("Stored {0} BPM", args.Value);
}
显然,在具有单个控制器的应用程序中,可以避免创建单独的模型对象和使用事件来执行控制流,但使用模型对象更适合实际应用。
运行示例应用
iOS 模拟器不支持 Health Kit。 必须在运行 iOS 8 的物理设备上完成调试。
将正确预配的 iOS 8 开发设备附加到系统。 在 Visual Studio for Mac 中选择它作为部署目标,然后从菜单中选择“运行”>“调试”。
重要
此时,与预配相关的错误将会显示出来。 若要排查错误,请查看上面的“创建和预配 Health Kit 应用”部分。 组件如下:
- iOS 开发人员中心 - 已启用显式应用 ID 和 Health Kit 的预配配置文件。
- 项目选项 - 捆绑标识符(显式应用 ID)和预配配置文件。
- 源代码 - Entitlements.plist 和 Info.plist
假设已正确设置规定,应用将会启动。 当它到达其 OnActivated
方法时,它将请求 Health Kit 授权。 操作系统首次遇到这种情况时,用户将看到以下对话框:
使应用能够更新心率数据,应用将重新出现。 将异步激活 ReactToHealthCarePermissions
回调。 这将导致HeartRateModel’s
Enabled
属性发生更改,这会引发EnabledChanged
事件,这将导致HKPermissionsViewController.OnEnabledChanged()
事件处理程序运行,从而启用StoreData
该按钮。 下图显示了顺序:
按“记录”按钮。 这会导致 StoreData_TouchUpInside()
处理程序运行,该处理程序将尝试分析 heartRate
文本字段的值,通过前面所述的 HeartRateModel.HeartRateInBeatsPerMinute()
函数转换为 HKQuantity
,并将该数量传递给 HeartRateModel.StoreHeartRate()
。 如前所述,这会尝试存储数据并引发 HeartRateStored
或 ErrorMessageChanged
事件。
双击设备上的“主页”按钮并打开“健康”应用。 单击“源”选项卡,你将看到列出的示例应用。 选择它并禁止授予更新心率数据的权限。 双击“主页”按钮并切换回应用。 将再次调用 ReactToHealthCarePermissions()
,但这一次,由于访问被拒绝,“StoreData”按钮将变为已禁用状态(请注意,这是异步发生的,用户界面中的更改可能对最终用户可见)。
先进主题
从 Health Kit 数据库读取数据与写入数据非常相似:指定尝试访问的数据类型,请求授权,如果授予了该授权,则提供数据,并自动转换为兼容的度量单位。
有许多更复杂的查询函数,它们允许基于谓词的查询以及在更新相关数据时执行更新的查询。
Health Kit 应用程序开发人员应查看 Apple 应用审查准则中的“Health Kit”部分。
理解安全性和类型系统模型后,在共享的 Health Kit 数据库中存储和读取数据就非常简单了。 Health Kit 中的许多功能都是异步运行的,应用程序开发人员必须正确编写他们的程序。
截至撰写本文时,Android 或 Windows Phone 中目前还没有与 Health Kit 相当的工具。
总结
在本文中,我们已了解 Health Kit 如何允许应用程序存储、检索和共享健康相关的信息,同时还提供了一个标准的健康应用,它允许用户访问和控制这些数据。
我们还了解了隐私、安全和数据完整性如何成为健康相关信息的首要考虑因素,并且使用 Health Kit 的应用必须处理好应用程序管理方面(预配)、编码(Health Kit 的类型系统)和用户体验(通过系统对话框和健康应用控制用户权限)方面日益提高的复杂性。
最后,我们使用随附的示例应用了解了 Health Kit 的简单实现,该应用将心跳数据写入 Health Kit 存储,并采用异步感知的设计。