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”页

在 Apple 开发人员门户中注册应用

如上图所示,创建应用说明后,使用显式“应用 ID 部分”为应用程序创建 ID。 在“应用服务”部分,选中“启用服务”部分中的“Health Kit”

完成后,按“继续”按钮在帐户中注册应用 ID。 你将返回到“证书、标识符和配置文件”页。 单击“预配配置文件”转到当前预配配置文件的列表,然后单击右上角的 + 按钮转到“添加 iOS 预配配置文件”页。 选择“iOS 应用开发”选项,然后单击“继续”转到“选择应用 ID”页。 在此处,请选择之前指定的显式应用 ID

选择显式应用 ID

单击“继续”并完成其余屏幕中的设置,需在其中指定此预配配置文件的“开发人员证书”、“设备”和“名称”

生成预配配置文件

单击“生成”并等待创建配置文件。 下载该文件,然后双击以在 Xcode 中安装。 可以在“Xcode”“首选项”“帐户”“查看详细信息...”下确认其安装状态。你应会看到刚刚安装的预配配置文件,并且其“Entitlements”行中应包含 Health Kit 和任何其他特殊服务的图标>>>

在 Xcode 中查看配置文件

将应用 ID 和预配配置文件与 Xamarin.iOS 应用相关联

如前所述创建并安装相应的预配配置文件后,通常需要在 Visual Studio for Mac 或 Visual Studio 中创建解决方案。 Health Kit 访问权限可用于任何 iOS C# 或 F# 项目。

无需手动完成创建 Xamarin iOS 8 项目的过程,可以打开本文附带的示例应用(其中包括预生成的情节提要和代码)。 若要将示例应用与已启用 Health Kit 的预配配置文件相关联,请在“Solution Pad”中右键单击你的项目并打开其“选项”对话框。 切换到“iOS 应用程序”面板,然后输入之前创建的显式应用 ID,作为应用的捆绑标识符

输入显式应用 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 对象图表

HKSampleType 是抽象的,有四个具体的子类。 目前只有一种类型的 HKCategoryType 数据,即睡眠分析。 Health Kit 中的绝大部分数据都是 HKQuantityType 类型,其数据存储在 HKQuantitySample 对象中,这些对象是使用熟悉的工厂设计模式创建的:

运行状况工具包中的大部分数据属于 HKQuantityType 类型,并将其数据存储在 HKQuantitySample 对象中

HKQuantityType 类型范围为 HKQuantityTypeIdentifier.ActiveEnergyBurnedHKQuantityTypeIdentifier.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 表达式的最终调用会触发相关事件:HeartRateStoredErrorMessageChanged

对模型进行编程后,接下来可以查看控制器如何对模型的状态做出反应。 打开 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’sEnabled 属性发生更改,从而引发 EnabledChanged 事件,进而导致 HKPermissionsViewController.OnEnabledChanged() 事件处理程序运行,从而启用 StoreData 按钮。 下图显示了顺序:

此图显示了事件序列

按“记录”按钮。 这会导致 StoreData_TouchUpInside() 处理程序运行,该处理程序将尝试分析 heartRate 文本字段的值,通过前面所述的 HeartRateModel.HeartRateInBeatsPerMinute() 函数转换为 HKQuantity,并将该数量传递给 HeartRateModel.StoreHeartRate()。 如前所述,这会尝试存储数据并引发 HeartRateStoredErrorMessageChanged 事件。

双击设备上的“主页”按钮并打开“健康”应用。 单击“源”选项卡,你将看到列出的示例应用。 选择它并禁止授予更新心率数据的权限。 双击“主页”按钮并切换回应用。 将再次调用 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 存储,并采用异步感知的设计。