Xamarin.iOS 中的 HealthKit

运行状况工具包可以安全地存储用户的运行状况相关信息数据。 得到用户明确许可后,运行状况工具包应用可以读取和写入此数据存储,并在添加相关数据时接收通知。 应用可以显示数据,或者用户可以使用 Apple 提供的运行状况应用查看其所有数据的仪表板。

由于与健康相关的数据非常敏感且至关重要,因此健康工具包具有强类型化,其度量单位和与记录的信息类型(例如血糖水平或心率)的明确关联。 此外,运行状况工具包应用必须使用显式权利,必须请求对特定类型信息的访问权限,并且用户必须显式授予应用对这些数据类型的访问权限。

本文将介绍:

  • 运行状况工具包的安全要求,包括应用程序预配和请求用户访问 Health Kit 数据库的权限;
  • Health Kit 的类型系统,可最大程度地减少错误应用或错误解释数据的可能性;
  • 写入共享的、系统范围的运行状况工具包数据存储。

本文不会介绍更高级的主题,例如查询数据库、在度量单位之间进行转换或接收新数据的通知。

在本文中,我们将创建一个示例应用程序来记录用户的心率:

A sample application to record the users heart rate

要求

完成本文中介绍的步骤需要满足以下条件:

  • Xcode 7 和 iOS 8 (或更高版本) - 需要在开发人员的计算机上安装和配置 Apple 的最新 Xcode 和 iOS API。
  • Visual Studio for Mac 或 Visual Studio – 应在开发人员的计算机上安装和配置最新版本的 Visual Studio for Mac。
  • iOS 8(或更高版本)设备 – 运行最新版本的 iOS 8 或更高版本的 iOS 设备进行测试。

重要

运行状况工具包已在 iOS 8 中引入。 目前,运行状况工具包在 iOS 模拟器上不可用,调试需要连接到物理 iOS 设备。

创建和预配运行状况工具包应用

在 Xamarin iOS 8 应用程序可以使用 HealthKit API 之前,必须正确配置和预配它。 本部分介绍正确设置 Xamarin 应用程序所需的步骤。

运行状况工具包应用需要:

  • 显式 应用 ID
  • 该显式应用 ID运行状况工具包权限关联的预配配置文件
  • Entitlements.plistBooleancom.apple.developer.healthkit类型设置为 Yes..
  • Info.plist一个键UIRequiredDeviceCapabilities包含具有值healthkit项的String项。
  • Info.plist还必须具有适当的隐私说明条目:String如果应用要写入数据String,则为密钥NSHealthUpdateUsageDescription提供解释;如果应用要读取运行状况工具包数据,则为密钥NSHealthShareUsageDescription提供说明。

若要详细了解如何预配 iOS 应用,Xamarin 入门系列中的设备预配文章介绍了开发人员证书、应用 ID、预配配置文件和应用权利之间的关系。

显式应用 ID 和预配配置文件

在 Apple 的 iOS 开发人员中心内创建显式应用 ID 和适当的预配配置文件

当前应用 ID 列在开发人员中心的“证书、标识符和配置文件”部分中。 通常,此列表将显示 ID 值的 ID 值*,指示应用 ID - 名称可以与任意数量的后缀一起使用。 此类 Wild卡应用 ID 不能与运行状况工具包一起使用。

若要创建显式 应用 ID,请单击右上角的 + 按钮,将你带到 “注册 iOS 应用 ID ”页:

Registering an app on the Apple Developer Portal

如上图所示,创建应用说明后,使用 “显式应用 ID ”部分为应用程序创建 ID。 在“App 服务”部分中,检查“启用服务”部分中的“运行状况工具包”。

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

Select the explicit App ID

单击“继续”并浏览其余屏幕,可在其中指定开发人员证书、设备(s)预配配置文件的名称

Generating the Provisioning Profile

单击“ 生成 ”并等待配置文件的创建。 下载该文件,然后双击该文件以在 Xcode 中安装。 可以在“Xcode > 首选项>帐户>”下确认其安装详细信息...应会看到刚刚安装的预配配置文件,并且它应具有运行状况工具包的图标及其权利行中的其他任何特殊服务:

Viewing the profile in Xcode

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

创建并安装相应的 预配配置文件 后,通常会在 Visual Studio for Mac 或 Visual Studio 中创建解决方案。 运行状况工具包访问可用于任何 iOS C# 或 F# 项目。

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

Enter the explicit App ID

现在切换到 iOS 捆绑签名 面板。 最近安装的 预配配置文件(其与显式 应用 ID 的关联)现在可用作 预配配置文件

Select the Provisioning Profile

如果预配配置文件不可用,检查 请双击 iOS 应用程序面板中的捆绑标识符,而不是 iOS 开发人员中心中指定的捆绑标识符,并安装预配配置文件Xcode > 首选项>帐户>查看详细信息...)。

选择启用运行状况工具包的预配配置文件后,单击“确定”关闭“项目选项”对话框。

Entitlements.plist 和 Info.plist 值

示例应用包括一个 Entitlements.plist 文件(这是启用运行状况工具包的应用所必需的),并且不包括在每个项目模板中。 如果项目不包含权利,请右键单击项目,选择“ 文件 > 新建文件...” > 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 ,其中包括所有必需的密钥。

编程运行状况工具包

运行状况工具包数据存储是一种专用用户特定的数据存储,可在应用之间共享。 由于运行状况信息非常敏感,因此用户必须采取积极步骤才能允许数据访问。 此访问权限可能是部分(写入但未读取、某些类型的数据的访问权限,但不是其他数据等)且随时可能吊销。 运行状况工具包应用程序应该以防御性的方式编写,并了解许多用户会犹豫存储其与运行状况相关的信息。

运行状况工具包数据仅限于 Apple 指定的类型。 这些类型是严格定义的:某些类型(如血液类型)仅限于苹果提供枚举的特定值,而另一些类型则将数量级与度量单位(如克、卡路里和升) 组合在一起。 即使是共享兼容度量单位的数据也由它们 HKObjectType区分;例如,类型系统将捕获错误地尝试将值存储 HKQuantityTypeIdentifier.NumberOfTimesFallen 到需要 HKQuantityTypeIdentifier.FlightsClimbed 度量单位的字段,即使两者都使用 HKUnit.Count 度量单位。

运行状况工具包数据存储中可存储的类型都是 < a0/> 的 HKObjectType子类。 HKCharacteristicType 对象存储生物性、血型和出生日期。 不过,更常见的是 HKSampleType 对象,这些对象表示在特定时间或一段时间内采样的数据。

HKSampleType objects chart

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

The large majority of data in Health Kit are of type HKQuantityType and store their data in HKQuantitySample objects

HKQuantityType类型范围从 .HKQuantityTypeIdentifier.ActiveEnergyBurnedHKQuantityTypeIdentifier.StepCount

从用户请求权限

最终用户必须采取积极步骤,允许应用读取或写入 Health Kit 数据。 这是通过 iOS 8 设备上预安装的 Health 应用完成的。 首次运行运行状况工具包应用时,用户会显示一个系统控制的 “运行状况访问 ”对话框:

The user is presented with a system-controlled Health Access dialog

稍后,用户可以使用 Health 应用的 “源 ”对话框更改权限:

The user can change permissions using Health apps Sources dialog

由于运行状况信息非常敏感,因此应用开发人员应以防御性的方式编写其程序,并期望在应用运行时拒绝和更改权限。 最常见的成语是在方法中 UIApplicationDelegate.OnActivated 请求权限,然后根据需要修改用户界面。

权限演练

在运行状况工具包预配的项目中,打开 AppDelegate.cs 该文件。 请注意使用 HealthKit;在文件顶部的语句。

以下代码与运行状况工具包权限相关:

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 指示与显示权限对话相关的某种错误。

重要

若要清楚地了解此函数的参数: 成功 参数和 错误 参数不指示用户是否已授予访问 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 - 在运行状况工具包数据库中存储心率时引发。

请注意,每当触发这些事件时,它都通过它 NSObject.InvokeOnMainThread()完成,这允许订阅者更新 UI。 或者,可以将事件记录为在后台线程上引发,并确保兼容性的责任可以留给其处理程序。 线程注意事项在运行状况工具包应用程序中很重要,因为许多函数(如权限请求)都是异步的,并在非主线程上执行其回调。

希思工具包中的特定代码 HeartRateModel 位于两个函数 HeartRateInBeatsPerMinute()StoreHeartRate().

HeartRateInBeatsPerMinute() 将其参数转换为强类型运行状况工具包 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 模拟器不支持运行状况工具包。 必须在运行 iOS 8 的物理设备上完成调试。

将正确预配的 iOS 8 开发设备附加到系统。 选择它作为 Visual Studio for Mac 中的部署目标,然后从菜单中选择“ 运行 > 调试”。

重要

此时,与预配相关的错误将浮出水面。 若要排查错误,请查看上面的“创建和预配运行状况工具包应用”部分。 组件如下:

  • iOS 开发人员中心 - 已启用显式应用 ID 和运行状况工具包的预配配置文件。
  • 项目选项 - 捆绑标识符 (显式应用 ID) 和预配配置文件。
  • 源代码 - Entitlements.plist & Info.plist

假设已正确设置预配,应用程序将启动。 当它到达其 OnActivated 方法时,它将请求运行状况工具包授权。 操作系统首次遇到此问题时,用户将显示以下对话框:

The user will be presented with this dialog

使应用能够更新心率数据,应用将重新出现。 回调 ReactToHealthCarePermissions 将以异步方式激活。 这将导致 HeartRateModel’sEnabled 属性发生更改,这会引发 EnabledChanged 事件,这将导致 HKPermissionsViewController.OnEnabledChanged() 事件处理程序运行,从而启用 StoreData 该按钮。 下图显示了序列:

This diagram shows the sequence of events

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

双击 设备上的“开始 ”按钮并打开“运行状况”应用。 单击“源”选项卡,将看到列出的示例应用。 选择它并禁止更新心率数据的权限。 双击“ 开始” 按钮并切换回应用。 将再次调用, ReactToHealthCarePermissions() 但这次,由于访问被拒绝, StoreData 按钮将变为禁用(请注意,这是异步发生的,用户界面中的更改可能对最终用户可见)。

先进主题

从运行状况工具包数据库中读取数据与写入数据非常相似:一个指定一个尝试访问的数据类型、请求授权以及授予该授权时,数据可用,并自动转换为兼容的度量单位。

有许多更复杂的查询函数,这些函数允许基于谓词的查询和查询在更新相关数据时执行更新。

运行状况工具包应用程序的开发人员应查看 Apple 应用评审指南“运行状况工具包”部分。

了解安全和类型系统模型后,在共享运行状况工具包数据库中存储和读取数据非常简单。 运行状况工具包中的许多函数以异步方式运行,应用程序开发人员必须相应地编写其程序。

在撰写本文时,目前没有与 Android 或 Windows 电话中的运行状况工具包等效。

总结

本文介绍了 Health Kit 如何允许应用程序存储、检索和共享运行状况相关信息,同时提供标准 Health 应用,允许用户访问和控制此数据。

我们还了解了隐私、安全和数据完整性如何覆盖使用 Health Kit 的运行状况相关信息和数据完整性的问题,这些问题必须处理应用程序管理方面(预配)、编码(运行状况工具包的类型系统)和用户体验(通过系统对话框和运行状况应用对权限的控制权)的复杂性增加。

最后,我们了解了使用包含的示例应用(将检测信号数据写入运行状况工具包存储并具有异步感知设计)的运行状况工具包的简单实现。