Xamarin.iOS 中的 HealthKit

Health Kit 提供使用者健康情況相關信息的安全數據存放區。 健康情況套件應用程式可能會具有使用者的明確許可權,讀取和寫入此資料存放區,並在新增相關數據時接收通知。 應用程式可以呈現數據,或者使用者可以使用Apple提供的Health應用程式來檢視其所有數據的儀錶板。

由於健康相關數據非常敏感且至關重要,因此 Health Kit 是強型別,具有測量單位和與所記錄資訊類型的明確關聯(例如血糖水準或心率)。 此外,Health Kit 應用程式必須使用明確的權利、要求存取特定類型的資訊,而且使用者必須明確授與應用程式對這些數據類型的存取權。

本文將介紹:

  • Health Kit 的安全性需求,包括應用程式佈建和要求用戶許可權來存取 Health Kit 資料庫;
  • 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 裝置進行測試。

重要

Health Kit 是在 iOS 8 中引進的。 目前,iOS 模擬器上無法使用 Health Kit,而且偵錯需要連線到實體 iOS 裝置。

建立和布建健康情況套件應用程式

在 Xamarin iOS 8 應用程式可以使用 HealthKit API 之前,必須先正確設定並布建它。 本節將涵蓋正確設定 Xamarin 應用程式所需的步驟。

Health Kit 應用程式需要:

  • 明確的 應用程式識別碼
  • 該明確應用程式識別碼和健康情況套件許可權相關聯的布建配置檔
  • Entitlements.plist,類型com.apple.developer.healthkitBoolean 的屬性設定為 Yes
  • Info.plist,其UIRequiredDeviceCapabilities索引鍵包含具有 值healthkit的專案String
  • Info.plist也必須有適當的隱私權說明專案:String如果應用程式要寫入數據,則為密鑰NSHealthUpdateUsageDescription提供說明,如果String應用程式要讀取 Health Kit 資料,則為密鑰NSHealthShareUsageDescription提供說明。

若要深入瞭解如何佈建 iOS 應用程式,Xamarin 快速入門系列中的裝置布建文章說明開發人員憑證、應用程式標識碼、布建配置檔和應用程式權利之間的關聯性。

明確應用程式識別碼和布建配置檔

在 Apple 的 iOS 開發人員中心 內,會建立明確的應用程式識別碼和適當的布建配置檔

您目前的應用程式識別碼會列在 開發人員中心 的 [憑證、標識碼和配置檔] 區段中。 此清單通常會顯示*識別碼值,指出應用程式 - 識別碼名稱可以搭配任意數目的後綴使用。 這類 通配符應用程式標識碼 無法與 Health Kit 搭配使用。

若要建立明確的 應用程式識別碼,請按下 + 右上角的按鈕,以帶您前往 [註冊 iOS 應用程式識別符 ] 頁面:

Registering an app on the Apple Developer Portal

如上圖所示,建立應用程式描述之後,請使用 [明確應用程式標識符 ] 區段來建立應用程式的標識碼。 在 [App Services] 區段中,檢查 [啟用服務] 區段中的 [健康情況套件]。

當您完成時,請按 [ 繼續] 按鈕,在您的帳戶中註冊 應用程式標識碼 。 您將回到 [憑證]、[標識符] 和 [配置檔] 頁面。 按兩下 [ 布建設定檔 ] 以帶您前往目前布建配置檔的清單,然後按下 + 右上角的按鈕,以帶您前往 [新增 iOS 布建配置檔 ] 頁面。 選取 [ iOS 應用程式開發 ] 選項,然後按兩下 [ 繼續 ] 以移至 [ 選取應用程式識別碼 ] 頁面。 在這裡,選取您先前指定的明確 應用程式識別碼

Select the explicit App ID

單擊 [繼續] 並完成其餘畫面,您可以在其中指定開發人員憑證、裝置(s),以及佈建配置檔的名稱

Generating the Provisioning Profile

按兩下 [產生 ] 並等候設定檔的建立。 下載檔案,然後按兩下它以在 Xcode 中安裝。 您可以在 [Xcode > 喜好設定>帳戶>檢視詳細數據] 底下確認是否已安裝...您應該會看到剛安裝的佈建設定檔,而且它應該有 Health Kit 及其權利資料列中任何其他特殊服務的圖示:

Viewing the profile in Xcode

將應用程式識別碼和布建配置檔與您的 Xamarin.iOS 應用程式產生關聯

建立並安裝適當的 布建配置檔 后,通常是時候在Visual Studio for Mac 或Visual Studio中建立解決方案了。 健康情況套件存取可供任何 iOS C# 或 F# 專案使用。

不逐步執行手動建立 Xamarin iOS 8 專案的程式,而是開啟附加至本文的範例應用程式(其中包括預先建置的 Storyboard 和程式代碼)。 若要將範例應用程式與已啟用健康情況套件的布建配置檔產生關聯,請在SolutionPad,以滑鼠右鍵按下您的專案,並顯示其 [選項] 對話框。 切換至 [iOS 應用程式] 面板,然後輸入您先前建立為應用程式套件組合識別碼的明確應用程式識別碼

Enter the explicit App ID

現在切換至 [iOS 套件組合簽署 ] 面板。 您最近安裝的佈建設定檔,與其與明確應用程式識別碼的關聯,現在會以布建配置檔的形式提供:

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應用程式的 必須具有與索引鍵相關聯的 UIRequiredDeviceCapabilitieshealthkit

<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
    <string>healthkit</string>
</array>

本文提供的範例應用程式包含預先設定 Entitlements.plist ,其中包含所有必要的密鑰。

程序設計健康情況套件

Health Kit 資料存放區是應用程式之間共用的私人使用者特定資料存放區。 由於健康情況資訊如此敏感,因此使用者必須採取積極步驟來允許數據存取。 此存取權可能是部分的(寫入但不是讀取、某些數據類型的存取權,但不是其他數據類型等等),而且可以隨時撤銷。 健康情況套件應用程式應該以防禦方式撰寫,瞭解許多使用者對於儲存其健康情況相關信息會猶豫不決。

Health Kit 資料僅限於 Apple 指定的類型。 這些類型是嚴格定義的:有些,如血液類型,僅限於蘋果提供列舉的特定值,而另一些則結合量級與測量單位(如克、卡路里和升)。 即使是共用相容度量單位的數據也會加以 HKObjectType區別;例如,類型系統會攔截錯誤嘗試將值儲存 HKQuantityTypeIdentifier.NumberOfTimesFallen 到預期 HKQuantityTypeIdentifier.FlightsClimbed 為 的欄位,即使兩者都使用 HKUnit.Count 量值單位也一樣。

儲存在 Health Kit 資料存放區中的類型都是 的 HKObjectType子類別。 HKCharacteristicType 對象會儲存生物性別、血型和出生日期。 不過,更常見的是 HKSampleType 物件,代表在特定時間或一段時間內取樣的數據。

HKSampleType objects chart

HKSampleType 是抽象的,而且有四個具體的子類別。 目前只有一種類型的 HKCategoryType 數據,也就是睡眠分析。 Health Kit 中大部分的數據類型為 , HKQuantityType 並將其數據儲存在 物件中 HKQuantitySample ,這些物件是使用熟悉的 Factory 設計模式所建立:

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 應用程式來完成。 第一次執行 Health Kit 應用程式時,使用者會看到系統控制的 健全狀況存取 對話框:

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

稍後,使用者可以使用 [健康情況] 應用程式的 [來源 ] 對話框來變更許可權:

The user can change permissions using Health apps Sources dialog

由於健康情況資訊非常敏感,應用程式開發人員應該以防禦方式撰寫其程式,並期望在應用程式執行時將拒絕和變更許可權。 最常見的慣用語是在 方法中 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()呼會在使用者與許可權對話框互動之後呼叫,並傳遞兩項資訊:booltrue如果使用者已與許可權對話框NSError互動,且如果非 Null,則表示與呈現許可權對話框相關的某種錯誤。

重要

若要清楚瞭解此函式的自變數: 成功錯誤 參數不會指出使用者是否已授與存取 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。 或者,事件可以記錄為在背景線程上引發,以及確保相容性的責任可以留給其處理程式。 健康情況套件應用程式中的線程考慮很重要,因為許多函式,例如許可權要求,都是異步的,並在非主要線程上執行其回呼。

中的 HeartRateModel Heath Kit 特定程式代碼位於兩個函式 HeartRateInBeatsPerMinute()StoreHeartRate()中。

HeartRateInBeatsPerMinute() 將自變數轉換成強型別的 Health Kit HKQuantity。 數量的類型是由 所 HKQuantityTypeIdentifierKey.HeartRate 指定,而數量單位會 HKUnit.CountHKUnit.Minute 以 (換句話說,單位是 每分鐘的節拍單位)。

StoreHeartRate() 式會採用 HKQuantity (在範例應用程式中,由 HeartRateInBeatsPerMinute() 建立的 。 若要驗證其數據,它會使用 HKQuantity.IsCompatible() 方法,這個方法會傳回 true 物件單位是否可以轉換成 自變數中的單位。 如果使用這個 建立 HeartRateInBeatsPerMinute() 的數量顯然會傳回 true,但如果已建立數量,則也會傳回 true ,例如 ,Beats Per Hour。 更常見的是, HKQuantity.IsCompatible() 可以用來驗證使用者或裝置可能輸入或顯示於一個測量系統(例如帝國單位)中的品質、距離和能量,但可能會儲存在另一個系統中(例如計量單位)。

驗證數量相容性之後, HKQuantitySample.FromType() Factory 方法會用來建立強型別 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 開發人員中心 - 已啟用明確應用程式識別碼和健康情況套件的布建配置檔。
  • 項目選項 - 套件組合識別碼 (明確應用程式識別子) 和布建配置檔。
  • 原始程式碼 - Entitlements.plist & Info.plist

假設已正確設定布建,您的應用程式將會啟動。 當它到達其 OnActivated 方法時,它會要求 Health Kit 授權。 第一次遇到作業系統時,您的使用者將會看到下列對話方塊:

The user will be presented with this dialog

讓您的 app 能夠更新心率數據,而您的應用程式將會重新出現。 回 ReactToHealthCarePermissions 呼將會以異步方式啟動。 這會導致 HeartRateModel’sEnabled 屬性變更,這會引發 EnabledChanged 事件,這會導致 HKPermissionsViewController.OnEnabledChanged() 事件處理程序執行,這會啟用 StoreData 按鈕。 下圖顯示順序:

This diagram shows the sequence of events

按 [ 記錄] 按鈕。 這會導致StoreData_TouchUpInside()處理程式執行,這會嘗試剖析文字欄位的值、透過先前討論HeartRateModel.HeartRateInBeatsPerMinute()heartRate函式轉換成 HKQuantity ,並將該數量傳遞至 HeartRateModel.StoreHeartRate()。 如先前所述,這會嘗試儲存數據,並引發 HeartRateStoredErrorMessageChanged 事件。

按兩下裝置上的 [ 首頁 ] 按鈕,然後開啟 [健康情況] 應用程式。 按兩下 [ 來源] 索引 標籤,您會看到列出的範例應用程式。 選擇它,不允許更新心率數據的許可權。 按兩下 [ 首頁] 按鈕,然後切換回您的應用程式。 再次呼叫 , ReactToHealthCarePermissions() 但這次因為存取遭到拒絕, 因此 StoreData 按鈕將會停用(請注意,這會以異步方式發生,而且使用者介面中的變更可能會對終端用戶可見)。

進階主題

從 Health Kit 資料庫讀取數據與寫入數據非常類似:一個指定一個嘗試存取的數據類型、要求授權,以及授與該授權時,可以使用數據,並自動轉換成相容的測量單位。

有一些更複雜的查詢函式,允許述詞型查詢和查詢在更新相關數據時執行更新。

Health Kit 應用程式的開發人員應該檢閱Apple應用程式檢閱指導方針Health Kit一節。

一旦瞭解安全性和類型系統模型,在共用的 Health Kit 資料庫中儲存和讀取數據相當簡單。 Health Kit 內的許多函式會以異步方式運作,應用程式開發人員必須適當地撰寫其程式。

本文撰寫時,目前沒有相當於Android或 Windows 電話中的Health Kit。

摘要

在本文中,我們已瞭解 Health Kit 如何允許應用程式儲存、擷取及共用健康情況相關信息,同時提供標準 Health 應用程式,讓使用者存取及控制此數據。

我們也了解隱私權、安全性和數據完整性如何覆寫健康情況相關信息和使用 Health Kit 的應用程式,必須處理應用程式管理層面(布建)、編碼(Health Kit 類型系統)和用戶體驗的複雜性增加(透過系統對話框和健康情況應用程式對許可權的控制權)。

最後,我們已使用包含的範例應用程式,將活動訊號數據寫入 Health Kit 存放區,並具有異步感知設計,來查看健康情況套件的簡單實作。