搭配 Xamarin.iOS 使用 iCloud

iOS 5 中的 iCloud 記憶體 API 可讓應用程式將使用者檔案和應用程式特定資料儲存到集中位置,並從所有使用者的裝置存取這些專案。

有四種類型的記憶體可用:

  • 索引鍵/值記憶體 - 在使用者的其他裝置上,與您的應用程式共用少量數據。

  • UIDocument 記憶體 - 使用 UIDocument 的子類別,將檔和其他數據儲存在使用者的 iCloud 帳戶中。

  • CoreData - SQLite 資料庫記憶體。

  • 個別檔案和目錄 - 用於直接在文件系統中管理許多不同的檔案。

本文件討論前兩種類型 - 機碼/值組和 UIDocument 子類別 - 以及如何在 Xamarin.iOS 中使用這些功能。

重要

Apple 提供工具協助開發人員適當地處理歐盟一般資料保護規定 (GDPR)。

需求

  • 最新穩定版本的 Xamarin.iOS
  • Xcode 10
  • Visual Studio for Mac 或 Visual Studio 2019。

準備 iCloud 開發

應用程式必須設定為在 Apple Provisioning Portal 和專案本身中使用 iCloud。 在開發 iCloud 之前(或試用範例),請遵循下列步驟。

若要正確設定應用程式以存取 iCloud:

  • 尋找您的 TeamID - 登入 developer.apple.com 並瀏覽 成員中心 > 您的帳戶 > 開發人員帳戶摘要 ,以取得您的小組標識碼(或單一開發人員的個別標識符)。 它會是 10 個字元字串( 例如A93A5CM278 ) - 這會構成「容器識別碼」的一部分。

  • 建立新的應用程式識別碼 - 若要建立應用程式識別符,請遵循裝置布建指南中一節中所述的步驟,並確定檢查 iCloud 作為允許的服務:

Check iCloud as an allowed service

  • 建立新的布建配置檔 - 若要建立布建配置檔,請遵循裝置布建指南中所述的步驟。

  • 將容器識別碼新增至 Entitlements.plist - 容器識別碼格式為 TeamID.BundleID。 如需詳細資訊, 請參閱使用權利 指南。

  • 設定項目屬性 - 在 Info.plist 檔案中,確定套件組合識別元符合建立應用程式識別碼時設定的套件組合識別符;iOS 套件組合簽署會使用布建配置檔,其中包含具有 iCloud App Service 的應用程式識別碼,以及選取的自定義權利檔案。 這全都可以在 Visual Studio 的 [項目屬性] 窗格下完成。

  • 在您的裝置上啟用 iCloud - 移至 設定 > iCloud,並確定裝置已登入。 選取並開啟 [文件與數據] 選項。

  • 您必須使用裝置來測試 iCloud - 它無法在模擬器上運作。 事實上,您真的需要兩部以上的裝置,全都使用相同的 Apple ID 登入,才能看到 iCloud 運作情形。

索引鍵/值 儲存體

索引鍵/值記憶體適用於使用者可能喜歡跨裝置保存的少量數據,例如他們在書籍或雜誌中檢視的最後一頁。 索引鍵/值記憶體不應該用於備份數據。

使用索引鍵/值記憶體時,有一些需要注意的限制:

  • 金鑰大小 上限 - 索引鍵名稱不能超過 64 個字節。

  • 最大值大小 - 您無法將超過 64 KB 儲存在單一值中。

  • 應用程式 的最大索引鍵/值存放區大小 - 應用程式最多只能儲存最多 64 KB 的索引鍵/值數據。 嘗試設定超過該限制的索引鍵將會失敗,而先前的值將會保存。

  • 數據類型 - 只能儲存字串、數位和布爾值等基本類型。

iCloudKeyValue 範例示範其運作方式。 範例程式代碼會為每個裝置建立名為 的金鑰:您可以在一個裝置上設定此金鑰,並監看值傳播至其他人。 它也會建立一個稱為「共用」的密鑰,可在任何裝置上編輯-如果您同時在許多裝置上編輯,iCloud 會決定哪個值「獲勝」(使用變更的時間戳),並傳播。

此螢幕快照顯示使用的範例。 從 iCloud 收到變更通知時,會在畫面底部的捲動文字檢視中列印,並在輸入欄位中更新。

The flow of messages between devices

設定和擷取數據

此程式代碼示範如何設定字串值。

var store = NSUbiquitousKeyValueStore.DefaultStore;
store.SetString("testkey", "VALUE IN THE CLOUD");  // key and value
store.Synchronize();

呼叫 Synchronize 可確保值只保存至本機磁碟記憶體。 iCloud 的同步處理會在背景發生,且無法由應用程式程式代碼「強制」。 透過良好的網路連線,同步處理通常會在5秒內發生,不過,如果網路不佳(或已中斷連線),更新可能需要更長的時間。

您可以使用下列程式代碼擷取值:

var store = NSUbiquitousKeyValueStore.DefaultStore;
display.Text = store.GetString("testkey");

此值會從本機數據存放區擷取 - 此方法不會嘗試連絡 iCloud 伺服器以取得「最新」值。 iCloud 會根據自己的排程更新本機數據存放區。

刪除資料

若要完全移除機碼/值組,請使用 Remove 方法,如下所示:

var store = NSUbiquitousKeyValueStore.DefaultStore;
store.Remove("testkey");
store.Synchronize();

觀察變更

應用程式也可以藉由將觀察者新增至 NSNotificationCenter.DefaultCenter,在iCloud變更值時接收通知。 下列來自 KeyValueViewController.cs 方法的程式ViewWillAppear 代碼示範如何接聽這些通知,並建立哪些密鑰已變更的清單:

keyValueNotification =
NSNotificationCenter.DefaultCenter.AddObserver (
    NSUbiquitousKeyValueStore.DidChangeExternallyNotification, notification => {
    Console.WriteLine ("Cloud notification received");
    NSDictionary userInfo = notification.UserInfo;

    var reasonNumber = (NSNumber)userInfo.ObjectForKey (NSUbiquitousKeyValueStore.ChangeReasonKey);
    nint reason = reasonNumber.NIntValue;

    var changedKeys = (NSArray)userInfo.ObjectForKey (NSUbiquitousKeyValueStore.ChangedKeysKey);
    var changedKeysList = new List<string> ();
    for (uint i = 0; i < changedKeys.Count; i++) {
        var key = changedKeys.GetItem<NSString> (i); // resolve key to a string
        changedKeysList.Add (key);
    }
    // now do something with the list...
});

然後,您的程式代碼可以使用已變更的索引鍵清單採取一些動作,例如更新其本機複本,或使用新的值更新UI。

可能的變更原因是:ServerChange (0)、InitialSyncChange (1)或 QuotaViolationChange (2)。 您可以視需要存取原因並執行不同的處理(例如,您可能需要移除某些密鑰,因為 QuotaViolationChange)。

檔 儲存體

iCloud Document 儲存體 的設計目的是管理對您的應用程式(以及使用者)很重要的數據。 它可以用來管理應用程式執行所需的檔案和其他數據,同時提供所有使用者裝置的 iCloud 備份和共用功能。

此圖顯示這一切如何一起運作。 每個裝置都有儲存在本機記憶體上的數據(UbiquityContainer),而操作系統的 iCloud 精靈會負責在雲端中傳送和接收數據。 所有對UbiquityContainer的檔案存取都必須透過 FilePresenter/FileCoordinator 來完成,以防止並行存取。 類別 UIDocument 會為您實作這些專案;此範例示範如何使用UIDocument。

The document storage overview

iCloudUIDoc 範例會實作包含單一文字欄位的簡單 UIDocument 子類別。 文字會在 中 UITextView 轉譯,而編輯會由iCloud傳播至其他裝置,並顯示紅色的通知訊息。 範例程式代碼不會處理更進階的 iCloud 功能,例如衝突解決。

此螢幕快照顯示範例應用程式 - 在變更文字並按 UpdateChangeCount 之後,檔會透過 iCloud 同步至其他裝置。

This screenshot shows the sample application after changing the text and pressing UpdateChangeCount

iCloudUIDoc 範例有五個部分:

  1. 存取 UbiquityContainer - 判斷 iCloud 是否已啟用,以及應用程式 iCloud 儲存區域的路徑是否已啟用。

  2. 建立UIDocument子類別 - 建立類別以在iCloud記憶體與模型對象之間中繼。

  3. 尋找並開啟 iCloud 檔 - 使用 NSFileManagerNSPredicate 來尋找 iCloud 檔並加以開啟。

  4. 顯示 iCloud 檔 - 公開來自的屬性 UIDocument ,讓您可以與 UI 控件互動。

  5. 儲存 iCloud 檔 - 確保 UI 中所做的變更會儲存至磁碟和 iCloud。

所有 iCloud 作業都會以異步方式執行(或應該執行),如此一來,它們就不會在等候發生某些事情時封鎖。 您會在範例中看到三種不同的方式來完成這項作業:

線程 - 在初始呼叫GetUrlForUbiquityContainerAppDelegate.FinishedLaunching,會在另一個線程上完成,以防止封鎖主要線程。

NotificationCenter - 在異步操作時註冊通知,例如 NSMetadataQuery.StartQuery 完成。

完成處理程式 ─ 傳入方法,以在異步操作完成時執行,例如 UIDocument.Open

存取 UbiquityContainer

使用 iCloud Document 儲存體 的第一個步驟是判斷是否啟用 iCloud,如果是,則為 “ubiquity 容器” 的位置(iCloud 啟用檔案儲存在裝置上的目錄)。

此程式代碼位於 AppDelegate.FinishedLaunching 範例的方法中。

// GetUrlForUbiquityContainer is blocking, Apple recommends background thread or your UI will freeze
ThreadPool.QueueUserWorkItem (_ => {
    CheckingForiCloud = true;
    Console.WriteLine ("Checking for iCloud");
    var uburl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer (null);
    // OR instead of null you can specify "TEAMID.com.your-company.ApplicationName"

    if (uburl == null) {
        HasiCloud = false;
        Console.WriteLine ("Can't find iCloud container, check your provisioning profile and entitlements");

        InvokeOnMainThread (() => {
            var alertController = UIAlertController.Create ("No \uE049 available",
            "Check your Entitlements.plist, BundleId, TeamId and Provisioning Profile!", UIAlertControllerStyle.Alert);
            alertController.AddAction (UIAlertAction.Create ("OK", UIAlertActionStyle.Destructive, null));
            viewController.PresentViewController (alertController, false, null);
        });
    } else { // iCloud enabled, store the NSURL for later use
        HasiCloud = true;
        iCloudUrl = uburl;
        Console.WriteLine ("yyy Yes iCloud! {0}", uburl.AbsoluteUrl);
    }
    CheckingForiCloud = false;
});

雖然範例沒有這麼做,但每當應用程式來到前景時,Apple 建議呼叫 GetUrlForUbiquityContainer。

建立UIDocument子類別

所有 iCloud 檔案和目錄(即儲存在 UbiquityContainer 目錄中的任何專案)都必須使用 NSFileManager 方法來管理,實作 NSFilePresenter 通訊協定,並透過 NSFileCoordinator 寫入。 執行所有作業的最簡單方式不是自行撰寫,而是子類別 UIDocument 為您完成所有作業。

您只需要在 UIDocument 子類別中實作兩種方法,才能使用 iCloud:

  • LoadFromContents - 傳入檔案內容的 NSData,以便您將模型類別/es 解壓縮。

  • ContentsForType - 要求您提供模型類別/es 的 NSData 表示法,以儲存至磁碟 (和 Cloud)。

iCloudUIDoc\MonkeyDocument.cs的這個範例程式代碼會示範如何實作 UIDocument。

public class MonkeyDocument : UIDocument
{
    // the 'model', just a chunk of text in this case; must easily convert to NSData
    NSString dataModel;
    // model is wrapped in a nice .NET-friendly property
    public string DocumentString {
        get {
            return dataModel.ToString ();
        }
        set {
            dataModel = new NSString (value);
        }
    }
    public MonkeyDocument (NSUrl url) : base (url)
    {
        DocumentString = "(default text)";
    }
    // contents supplied by iCloud to display, update local model and display (via notification)
    public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError)
    {
        outError = null;

        Console.WriteLine ("LoadFromContents({0})", typeName);

        if (contents != null)
            dataModel = NSString.FromData ((NSData)contents, NSStringEncoding.UTF8);

        // LoadFromContents called when an update occurs
        NSNotificationCenter.DefaultCenter.PostNotificationName ("monkeyDocumentModified", this);
        return true;
    }
    // return contents for iCloud to save (from the local model)
    public override NSObject ContentsForType (string typeName, out NSError outError)
    {
        outError = null;

        Console.WriteLine ("ContentsForType({0})", typeName);
        Console.WriteLine ("DocumentText:{0}",dataModel);

        NSData docData = dataModel.Encode (NSStringEncoding.UTF8);
        return docData;
    }
}

在此案例中,數據模型非常簡單 - 單一文字欄位。 您的數據模型可以視需要而複雜,例如 Xml 檔或二進位數據。 UIDocument 實作的主要角色是在模型類別與 NSData 表示法之間轉譯,以在磁碟上儲存/載入。

尋找和開啟 iCloud 檔

範例應用程式只會處理單一檔案 -test.txt ,因此AppDelegate.cs中的程式代碼會NSPredicate建立 並NSMetadataQuery特別尋找該檔名。 會 NSMetadataQuery 以異步方式執行,並在完成時傳送通知。 DidFinishGathering 會由通知觀察者呼叫,停止查詢並呼叫LoadDocument,它會使用 UIDocument.Open 方法搭配完成處理程式嘗試載入檔案,並將其顯示在中 MonkeyDocumentViewController

string monkeyDocFilename = "test.txt";
void FindDocument ()
{
    Console.WriteLine ("FindDocument");
    query = new NSMetadataQuery {
        SearchScopes = new NSObject [] { NSMetadataQuery.UbiquitousDocumentsScope }
    };

    var pred = NSPredicate.FromFormat ("%K == %@", new NSObject[] {
        NSMetadataQuery.ItemFSNameKey, new NSString (MonkeyDocFilename)
    });

    Console.WriteLine ("Predicate:{0}", pred.PredicateFormat);
    query.Predicate = pred;

    NSNotificationCenter.DefaultCenter.AddObserver (
        this,
        new Selector ("queryDidFinishGathering:"),
        NSMetadataQuery.DidFinishGatheringNotification,
        query
    );

    query.StartQuery ();
}

[Export ("queryDidFinishGathering:")]
void DidFinishGathering (NSNotification notification)
{
    Console.WriteLine ("DidFinishGathering");
    var metadataQuery = (NSMetadataQuery)notification.Object;
    metadataQuery.DisableUpdates ();
    metadataQuery.StopQuery ();

    NSNotificationCenter.DefaultCenter.RemoveObserver (this, NSMetadataQuery.DidFinishGatheringNotification, metadataQuery);
    LoadDocument (metadataQuery);
}

void LoadDocument (NSMetadataQuery metadataQuery)
{
    Console.WriteLine ("LoadDocument");

    if (metadataQuery.ResultCount == 1) {
        var item = (NSMetadataItem)metadataQuery.ResultAtIndex (0);
        var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);
        doc = new MonkeyDocument (url);

        doc.Open (success => {
            if (success) {
                Console.WriteLine ("iCloud document opened");
                Console.WriteLine (" -- {0}", doc.DocumentString);
                viewController.DisplayDocument (doc);
            } else {
                Console.WriteLine ("failed to open iCloud document");
            }
        });
    } // TODO: if no document, we need to create one
}

顯示 iCloud 檔

顯示UIDocument不應該與任何其他模型類別不同 - 屬性會顯示在UI控制件中,可能由使用者編輯,然後寫回模型。

在 iCloudUIDoc\MonkeyDocumentViewController.cs範例中,會在 中UITextView顯示 MonkeyDocument 文字。 ViewDidLoad 接聽 方法中傳送的 MonkeyDocument.LoadFromContents 通知。 LoadFromContents 會在 iCloud 有檔案的新數據時呼叫,讓通知指出檔已更新。

NSNotificationCenter.DefaultCenter.AddObserver (this,
    new Selector ("dataReloaded:"),
    new NSString ("monkeyDocumentModified"),
    null
);

範例程式代碼通知處理程式會呼叫 方法來更新UI -在此情況下,沒有任何衝突偵測或解決方式。

[Export ("dataReloaded:")]
void DataReloaded (NSNotification notification)
{
    doc = (MonkeyDocument)notification.Object;
    // we just overwrite whatever was being typed, no conflict resolution for now
    docText.Text = doc.DocumentString;
}

儲存 iCloud 檔

若要將 UIDocument 新增至 iCloud,您可以直接呼叫 UIDocument.Save (僅適用於新檔),或使用 來移動現有的檔案 NSFileManager.DefaultManager.SetUbiquitious。 範例程式代碼會直接在此程式代碼的 ubiquity 容器中建立新檔(這裡有兩個完成處理程式,一個用於作業,另一個用於 Save Open):

var docsFolder = Path.Combine (iCloudUrl.Path, "Documents"); // NOTE: Documents folder is user-accessible in Settings
var docPath = Path.Combine (docsFolder, MonkeyDocFilename);
var ubiq = new NSUrl (docPath, false);
var monkeyDoc = new MonkeyDocument (ubiq);
monkeyDoc.Save (monkeyDoc.FileUrl, UIDocumentSaveOperation.ForCreating, saveSuccess => {
Console.WriteLine ("Save completion:" + saveSuccess);
if (saveSuccess) {
    monkeyDoc.Open (openSuccess => {
        Console.WriteLine ("Open completion:" + openSuccess);
        if (openSuccess) {
            Console.WriteLine ("new document for iCloud");
            Console.WriteLine (" == " + monkeyDoc.DocumentString);
            viewController.DisplayDocument (monkeyDoc);
        } else {
            Console.WriteLine ("couldn't open");
        }
    });
} else {
    Console.WriteLine ("couldn't save");
}

文件的後續變更不會直接「儲存」,而是告知 UIDocument 檔已使用 UpdateChangeCount變更,且會自動排程儲存至磁碟作業:

doc.UpdateChangeCount (UIDocumentChangeKind.Done);

管理 iCloud 檔

用戶可以透過 設定,在應用程式外部的 「ubiquity 容器」 的 Documents 目錄中管理 iCloud 檔;他們可以檢視檔案清單並撥動以刪除。 應用程式程式代碼應該能夠處理使用者刪除檔案的情況。 請勿將內部應用程式資料儲存在 Documents 目錄中。

Managing iCloud Documents workflow

當使用者嘗試從裝置移除已啟用 iCloud 的應用程式時,也會收到不同的警告,以通知他們與該應用程式相關的 iCloud 檔案狀態。

Screenshot shows a warning for Document Updates Pending.

Screenshot shows a warning for Delete i Cloud.

iCloud 備份

雖然備份至 iCloud 不是開發人員直接存取的功能,但您設計應用程式的方式可能會影響用戶體驗。 Apple 提供 iOS 資料 儲存體 指導方針,讓開發人員在其 iOS 應用程式中遵循。

最重要的考慮是您的應用程式是否儲存非用戶產生的大型檔案(例如,每期儲存數百兆位元組內容的雜誌讀者應用程式)。 Apple 偏好不要將這類數據儲存到 iCloud,而且不必要地填滿使用者的 iCloud 配額。

儲存大量數據的應用程式應該將它儲存在其中一個未備份的用戶目錄中(例如。快取或 tmp)或用來 NSFileManager.SetSkipBackupAttribute 將旗標套用至那些檔案,讓 iCloud 在備份作業期間忽略它們。

摘要

本文介紹 iOS 5 中包含的新 iCloud 功能。 它檢查了設定專案以使用 iCloud 所需的步驟,然後提供了如何實作 iCloud 功能的範例。

機碼/值記憶體範例示範如何使用 iCloud 來儲存與 NSUserPreferences 儲存方式類似的少量數據。 UIDocument 範例示範如何透過 iCloud 跨多個裝置儲存和同步處理更複雜的數據。

最後,它包含一個簡短的討論,說明如何新增 iCloud 備份會影響您的應用程式設計。