将 iCloud 与 Xamarin.iOS 配合使用
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 预配门户和项目本身中使用 iCloud。 在针对 iCloud 进行开发(或尝试示例)之前,请按照以下步骤操作。
若要正确配置一个用于访问 iCloud 的应用程序,请执行以下操作:
查找 TeamID - 登录 developer.apple.com 并访问“会员中心”>“你的帐户”>“开发人员帐户摘要”以获取团队 ID(若是单个开发人员,则为个人 ID)。 它将是一个 10 个字符的字符串(例如 A93A5CM278) - 构成“容器标识符”的一部分。
创建新的应用 ID - 若要创建应用 ID,请按照“设备预配”指南的“存储技术预配”部分中概述的步骤操作,并务必选中“iCloud”作为允许的服务:
创建新的预配配置文件 - 若要创建预配配置文件,请按照“设备预配”指南中概述的步骤操作。
将容器标识符添加到 Entitlements.plist - 容器标识符格式为
TeamID.BundleID
。 有关详细信息,请参阅使用权利指南。配置项目属性 - 在 Info.plist 文件中,确保“捆绑包标识符”与创建应用 ID 时设置的“捆绑包 ID”匹配;iOS 捆绑包签名使用一个“预配配置文件”,其中包含具有 iCloud 应用服务的应用 ID 以及所选的“自定义权利”文件。 这一切都可以在 Visual Studio 中的项目属性窗格下完成。
在设备上启用 iCloud - 转到“设置”>“iCloud”,确保设备已登录。 选择并打开“文档和数据”选项。
必须使用设备来测试 iCloud - 它无法在模拟器上运行。 事实上,你确实需要至少两台设备全都使用同一 Apple ID 登录才能看到 iCloud 的运行情况。
键值存储
键值存储适用于用户可能希望将其跨设备保存的少量数据,例如用户在书籍或杂志中看到的最后一页。 键值存储不应该用于备份数据。
使用键值存储时需要注意一些限制:
最大键大小 - 键名不能超过 64 字节。
最大值大小 - 单个值中存储的大小不能超过 64 KB。
应用的最大键值存储大小 - 应用程序总共最多只能存储 64 KB 的键值数据。 如果尝试设置超出该限制的键,则会遭遇失败,但先前的值会保留。
数据类型 - 只能存储字符串、数字和布尔值等基本类型。
iCloudKeyValue 示例演示了其工作原理。 示例代码为每个设备创建一个命名的键:你可以在一个设备上设置此键,然后观察该值传播到其他设备。 它还创建一个名为“共享”的键,该键可以在任何设备上进行编辑 - 如果你同时在多个设备上进行编辑,则由 iCloud 决定哪个值有效(依据该更改对应的时间戳)并让其进行传播。
此屏幕截图显示了正在使用的示例。 从 iCloud 收到更改通知时,这些通知会输出到屏幕底部的滚动文本视图中,并在输入字段中获得更新。
设置和检索数据
以下代码演示如何设置字符串值。
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.csViewWillAppear
方法中的以下代码演示如何侦听这些通知并创建已更改密钥的列表:
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 文档存储旨在管理对应用(和用户)重要的数据。 它可用于管理应用运行所需的文件和其他数据,同时在所有用户设备上提供基于 iCloud 的备份和共享功能。
下图显示了这一切是如何协作的。 每个设备都将数据保存在本地存储 (UbiquityContainer) 中,操作系统的 iCloud 守护程序负责在云端发送和接收数据。 对 UbiquityContainer 的所有文件访问都必须通过 FilePresenter/FileCoordinator 完成,以防止并发访问。 UIDocument
类为你实现这些;这个例子展示了如何使用 UIDocument。
iCloudUIDoc 示例实现了一个简单的 UIDocument
子类,其中包含单个文本字段。 文本在 UITextView
中呈现,编辑内容由 iCloud 传播到其他设备,通知消息显示为红色。 示例代码不处理解决冲突之类的更高级 iCloud 功能。
以下屏幕截图显示了示例应用程序 - 更改文本并按 UpdateChangeCount 后,文档将通过 iCloud 同步到其他设备。
iCloudUIDoc 示例有五个部分:
访问 UbiquityContainer - 确定 iCloud 是否已启用,如果已启用,则确定应用程序的 iCloud 存储区域的路径。
创建 UIDocument 子类 - 创建一个类来充当 iCloud 存储和你的模型对象之间的中介。
找到并打开 iCloud 文档 - 使用
NSFileManager
和NSPredicate
找到并打开 iCloud 文档。显示 iCloud 文档 - 公开
UIDocument
中的属性,以便你可以与 UI 控件进行交互。保存 iCloud 文档 - 确保在 UI 中所做的更改保存到磁盘和 iCloud。
所有 iCloud 操作都异步运行(或应该异步运行),因此,它们在等待要发生的操作时不会造成阻塞。 你将在示例中看到实现此目的的三种不同方法:
线程 - 在 AppDelegate.FinishedLaunching
中,对 GetUrlForUbiquityContainer
的初始调用是在另一个线程上完成的,目的是防止阻塞主线程。
NotificationCenter - 在异步操作(例如 NSMetadataQuery.StartQuery
)完成时注册通知。
完成事件处理器 - 传入要在完成异步操作(如 UIDocument.Open
)时运行的方法。
访问 UbiquityContainer
使用 iCloud 文档存储的第一步是确定 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,方便你将其解压到模型类中。
ContentsForType - 请求你提供模型类的 NSData 表示形式,以便将其保存到磁盘(和云)。
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,而 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
方法中发送的通知。 当 iCloud 有文件的新数据时,会调用 LoadFromContents
,以便通知指示文档已更新。
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 目录中。
尝试从设备中删除已启用 iCloud 的应用程序时,用户还会收到不同的警告,这些警告会告知用户与该应用程序相关的 iCloud 文档的状态。
iCloud 备份
虽然备份到 iCloud 不是一项由开发人员直接访问的功能,但你设计应用程序的方式可能会影响用户体验。 Apple 提供了 iOS 数据存储指南,供开发人员在其 iOS 应用程序中遵循。
最重要的考虑因素是应用是否存储非用户生成的大型文件(例如,每期存储一百多兆字节内容的杂志阅读器应用程序)。 Apple 不希望你将此类数据存储在一个会将数据备份到 iCloud 并因而会不必要地消耗用户的 iCloud 配额的位置。
存储大量此类数据的应用程序应将其存储在未备份的用户目录之一(例如 Caches 或 tmp)中,或者使用 NSFileManager.SetSkipBackupAttribute
对这些文件应用一个标志,使 iCloud 在备份操作过程中忽略它们。
总结
本文介绍了 iOS 5 中包含的新 iCloud 功能。 本文检查了将项目配置为使用 iCloud 所需的步骤,然后提供了有关如何实现 iCloud 功能的示例。
键值存储示例演示了如何使用 iCloud 来存储少量数据,类似于存储 NSUserPreference 的方式。 UIDocument 示例展示了如何通过 iCloud 在多个设备之间存储和同步更复杂的数据。
最后简要讨论了添加 iCloud 备份会如何影响应用程序设计。