Xamarin.iOS 中的文档选取器

可以通过文档选取器在应用之间共享文档。 这些文档可以存储在 iCloud 或其他应用的目录中。 文档通过用户在其设备上安装的一组文档提供程序扩展共享。

由于难以使文档跨应用和云保持同步,因此它们带来了一定程度的必要复杂性。

要求

要完成本文所述的步骤,需要满足以下条件:

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

对 iCloud 的更改

若要实现文档选取器的新功能,对 Apple 的 iCloud 服务进行了以下更改:

  • iCloud 守护程序已使用 CloudKit 完全重写。
  • 现有的 iCloud 功能已重命名为“iCloud 云盘”。
  • 已将对 Microsoft Windows OS 的支持添加到 iCloud。
  • 在 Mac OS Finder 中添加了一个 iCloud 文件夹。
  • iOS 设备可以访问 Mac OS iCloud 文件夹的内容。

重要

Apple 提供工具,用于帮助开发人员正确处理欧盟一般数据保护条例 (GDPR)。

什么是文档?

在 iCloud 中引用文档时,它是一个独立实体,用户应将其视为此类实体。 用户可能希望修改文档或与其他用户共享文档(例如,通过电子邮件)。

有几种类型的文件用户会立即识别为文档,例如 Pages、Keynote 或 Numbers 文件。 但是,iCloud 并不局限于此概念。 例如,游戏(如国际象棋比赛)的状态可以视为文档并存储在 iCloud 中。 此文件可以在用户的设备之间传递,并允许他们在其他设备上继续之前停止的游戏。

处理文档

在深入研究将文档选取器与 Xamarin 一起使用所需的代码之前,本文将介绍使用 iCloud 文档的最佳做法,以及对支持文档选取器所需的现有 API 进行的一些修改。

使用文件协调

由于可以从多个不同的位置修改文件,因此必须使用协调来防止数据丢失。

使用文件协调

我们来看看上面的图示:

  1. 使用文件协调的 iOS 设备会创建新的文档并将其保存到 iCloud 文件夹。
  2. iCloud 将修改后的文件保存到云中,以便分发到每个设备。
  3. 附加的 Mac 会在 iCloud 文件夹中看到修改的文件,并使用文件协调将更改复制到该文件。
  4. 不使用文件协调的设备会更改文件并将其保存到 iCloud 文件夹。 这些更改会立即复制到其他设备。

假设原始 iOS 设备或 Mac 正在编辑文件,现在其更改会丢失,并被未协调设备中的文件版本覆盖。 为了防止数据丢失,在使用基于云的文档时必须进行文件协调。

使用 UIDocument

UIDocument 通过为开发人员完成所有繁重的工作,使事情变得简单(或 macOS 上的 NSDocument)。 它提供内置的文件协调与后台队列,以防止阻止应用程序的 UI。

UIDocument 公开了多个高级 API,它们可简化 Xamarin 应用程序的开发工作,满足开发人员的任何需求。

以下代码会创建 UIDocument 的一个子类,以实现一个通用的基于文本的文档,该文档可用于存储和检索 iCloud 中的文本:

using System;
using Foundation;
using UIKit;

namespace DocPicker
{
    public class GenericTextDocument : UIDocument
    {
        #region Private Variable Storage
        private NSString _dataModel;
        #endregion

        #region Computed Properties
        public string Contents {
            get { return _dataModel.ToString (); }
            set { _dataModel = new NSString(value); }
        }
        #endregion

        #region Constructors
        public GenericTextDocument (NSUrl url) : base (url)
        {
            // Set the default document text
            this.Contents = "";
        }

        public GenericTextDocument (NSUrl url, string contents) : base (url)
        {
            // Set the default document text
            this.Contents = contents;
        }
        #endregion

        #region Override Methods
        public override bool LoadFromContents (NSObject contents, string typeName, out NSError outError)
        {
            // Clear the error state
            outError = null;

            // Were any contents passed to the document?
            if (contents != null) {
                _dataModel = NSString.FromData( (NSData)contents, NSStringEncoding.UTF8 );
            }

            // Inform caller that the document has been modified
            RaiseDocumentModified (this);

            // Return success
            return true;
        }

        public override NSObject ContentsForType (string typeName, out NSError outError)
        {
            // Clear the error state
            outError = null;

            // Convert the contents to a NSData object and return it
            NSData docData = _dataModel.Encode(NSStringEncoding.UTF8);
            return docData;
        }
        #endregion

        #region Events
        public delegate void DocumentModifiedDelegate(GenericTextDocument document);
        public event DocumentModifiedDelegate DocumentModified;

        internal void RaiseDocumentModified(GenericTextDocument document) {
            // Inform caller
            if (this.DocumentModified != null) {
                this.DocumentModified (document);
            }
        }
        #endregion
    }
}

在 Xamarin.iOS 8 应用程序中使用文档选取器和外部文档时,本文将使用上述 GenericTextDocument 类。

异步文件协调

iOS 8 通过新的文件协调 API 提供了多个新的异步文件协调功能。 在 iOS 8 之前,所有现有的文件协调 API 都是完全同步的。 这意味着开发人员负责实现自己的后台队列,以防止文件协调阻止应用程序的 UI。

新的 NSFileAccessIntent 类包含指向文件的 URL,以及用于控制所需协调类型的多个选项。 以下代码演示了如何使用意图将文件从一个位置移到另一个位置:

// Get source options
var srcURL = NSUrl.FromFilename ("FromFile.txt");
var srcIntent = NSFileAccessIntent.CreateReadingIntent (srcURL, NSFileCoordinatorReadingOptions.ForUploading);

// Get destination options
var dstURL = NSUrl.FromFilename ("ToFile.txt");
var dstIntent = NSFileAccessIntent.CreateReadingIntent (dstURL, NSFileCoordinatorReadingOptions.ForUploading);

// Create an array
var intents = new NSFileAccessIntent[] {
    srcIntent,
    dstIntent
};

// Initialize a file coordination with intents
var queue = new NSOperationQueue ();
var fileCoordinator = new NSFileCoordinator ();
fileCoordinator.CoordinateAccess (intents, queue, (err) => {
    // Was there an error?
    if (err!=null) {
        Console.WriteLine("Error: {0}",err.LocalizedDescription);
    }
});

发现和列出文档

发现和列出文档的方法是使用现有的 NSMetadataQuery API。 本部分将介绍添加到 NSMetadataQuery 中的新功能,这些功能使使用文档变得比以前更容易。

现有行为

在 iOS 8 之前,NSMetadataQuery 选取本地文件更改的速度很慢,例如:删除、创建和重命名。

NSMetadataQuery 本地文件更改概述

在上图中:

  1. 对于应用程序容器中已存在的文件,NSMetadataQuery 具有预先创建和后台处理的现有 NSMetadata 记录,以便它们立即可供应用程序使用。
  2. 应用程序会在应用程序容器中创建一个新文件。
  3. NSMetadataQuery 看到对应用程序容器的修改并创建所需的 NSMetadata 记录之前存在延迟。

由于 NSMetadata 记录的创建延迟,应用程序必须打开两个数据源:一个用于本地文件更改,一个用于基于云的更改。

拼结

在 iOS 8 中,NSMetadataQuery 更易于直接与名为“拼结”的新功能配合使用:

NSMetadataQuery,具有名为“拼结”的新功能

在上图中使用拼结:

  1. 与以前一样,对于应用程序容器中已存在的文件,NSMetadataQuery 具有预先创建和后台处理的现有 NSMetadata 记录。
  2. 应用程序会使用文件协调在应用程序容器中创建一个新文件。
  3. 应用程序容器中的挂钩将看到修改,并调用 NSMetadataQuery 来创建所需的 NSMetadata 记录。
  4. NSMetadata 记录直接在文件后创建,可供应用程序使用。

通过使用“拼结”,应用程序不再需要打开数据源来监视本地和基于云的文件更改。 现在,应用程序可以直接依赖于 NSMetadataQuery

重要

仅当应用程序使用文件协调(如上一部分所示)时,拼结才有效。 如果未使用文件协调,则 API 默认为 iOS 8 之前的现有行为。

新的 iOS 8 元数据功能

以下新功能已添加到 iOS 8 中的 NSMetadataQuery

  • NSMetatadataQuery 现在可以列出存储在云中的非本地文档。
  • 添加了新的 API 来访问基于云的文档上的元数据信息。
  • 有一个新的 NSUrl_PromisedItems API,它将访问内容可能在本地可用或不在本地可用的文件的文件属性。
  • 使用 GetPromisedItemResourceValue 方法获取有关给定文件的信息,或使用 GetPromisedItemResourceValues 方法一次获取有关多个文件的信息。

添加了两个新的文件协调标志来处理元数据:

  • NSFileCoordinatorReadImmediatelyAvailableMetadataOnly
  • NSFileCoordinatorWriteContentIndependentMetadataOnly

使用上述标志时,文档文件的内容不需要在本地可供使用。

以下代码段演示了如何使用 NSMetadataQuery 查询特定文件是否存在,并生成该文件(如果不存在):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Foundation;
using UIKit;
using ObjCRuntime;
using System.IO;

#region Static Properties
public const string TestFilename = "test.txt";
#endregion

#region Computed Properties
public bool HasiCloud { get; set; }
public bool CheckingForiCloud { get; set; }
public NSUrl iCloudUrl { get; set; }

public GenericTextDocument Document { get; set; }
public NSMetadataQuery Query { get; set; }
#endregion

#region Private Methods
private void FindDocument () {
    Console.WriteLine ("Finding Document...");

    // Create a new query and set it's scope
    Query = new NSMetadataQuery();
    Query.SearchScopes = new NSObject [] {
                NSMetadataQuery.UbiquitousDocumentsScope,
                NSMetadataQuery.UbiquitousDataScope,
                NSMetadataQuery.AccessibleUbiquitousExternalDocumentsScope
            };

    // Build a predicate to locate the file by name and attach it to the query
    var pred = NSPredicate.FromFormat ("%K == %@"
        , new NSObject[] {
            NSMetadataQuery.ItemFSNameKey
            , new NSString(TestFilename)});
    Query.Predicate = pred;

    // Register a notification for when the query returns
    NSNotificationCenter.DefaultCenter.AddObserver (this,
            new Selector("queryDidFinishGathering:"),             NSMetadataQuery.DidFinishGatheringNotification,
            Query);

    // Start looking for the file
    Query.StartQuery ();
    Console.WriteLine ("Querying: {0}", Query.IsGathering);
}

[Export("queryDidFinishGathering:")]
public void DidFinishGathering (NSNotification notification) {
    Console.WriteLine ("Finish Gathering Documents.");

    // Access the query and stop it from running
    var query = (NSMetadataQuery)notification.Object;
    query.DisableUpdates();
    query.StopQuery();

    // Release the notification
    NSNotificationCenter.DefaultCenter.RemoveObserver (this
        , NSMetadataQuery.DidFinishGatheringNotification
        , query);

    // Load the document that the query returned
    LoadDocument(query);
}

private void LoadDocument (NSMetadataQuery query) {
    Console.WriteLine ("Loading Document...");    

    // Take action based on the returned record count
    switch (query.ResultCount) {
    case 0:
        // Create a new document
        CreateNewDocument ();
        break;
    case 1:
        // Gain access to the url and create a new document from
        // that instance
        NSMetadataItem item = (NSMetadataItem)query.ResultAtIndex (0);
        var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);

        // Load the document
        OpenDocument (url);
        break;
    default:
        // There has been an issue
        Console.WriteLine ("Issue: More than one document found...");
        break;
    }
}
#endregion

#region Public Methods
public void OpenDocument(NSUrl url) {

    Console.WriteLine ("Attempting to open: {0}", url);
    Document = new GenericTextDocument (url);

    // Open the document
    Document.Open ( (success) => {
        if (success) {
            Console.WriteLine ("Document Opened");
        } else
            Console.WriteLine ("Failed to Open Document");
    });

    // Inform caller
    RaiseDocumentLoaded (Document);
}

public void CreateNewDocument() {
    // Create path to new file
    // var docsFolder = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
    var docsFolder = Path.Combine(iCloudUrl.Path, "Documents");
    var docPath = Path.Combine (docsFolder, TestFilename);
    var ubiq = new NSUrl (docPath, false);

    // Create new document at path
    Console.WriteLine ("Creating Document at:" + ubiq.AbsoluteString);
    Document = new GenericTextDocument (ubiq);

    // Set the default value
    Document.Contents = "(default value)";

    // Save document to path
    Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForCreating, (saveSuccess) => {
        Console.WriteLine ("Save completion:" + saveSuccess);
        if (saveSuccess) {
            Console.WriteLine ("Document Saved");
        } else {
            Console.WriteLine ("Unable to Save Document");
        }
    });

    // Inform caller
    RaiseDocumentLoaded (Document);
}

public bool SaveDocument() {
    bool successful = false;

    // Save document to path
    Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForOverwriting, (saveSuccess) => {
        Console.WriteLine ("Save completion: " + saveSuccess);
        if (saveSuccess) {
            Console.WriteLine ("Document Saved");
            successful = true;
        } else {
            Console.WriteLine ("Unable to Save Document");
            successful=false;
        }
    });

    // Return results
    return successful;
}
#endregion

#region Events
public delegate void DocumentLoadedDelegate(GenericTextDocument document);
public event DocumentLoadedDelegate DocumentLoaded;

internal void RaiseDocumentLoaded(GenericTextDocument document) {
    // Inform caller
    if (this.DocumentLoaded != null) {
        this.DocumentLoaded (document);
    }
}
#endregion

文档缩略图

Apple 认为,在为应用程序列出文档时,最好的用户体验是使用预览。 这为最终用户提供了上下文,因此他们可以快速识别要使用的文档。

在 iOS 8 之前,显示文档预览需要自定义实现。 iOS 8 的新增功能是文件系统属性,可让开发人员快速使用文档缩略图。

检索文档缩略图

通过调用 GetPromisedItemResourceValueGetPromisedItemResourceValues 方法,将返回 NSUrl_PromisedItems API (NSUrlThumbnailDictionary)。 此字典中当前唯一的键是 NSThumbnial1024X1024SizeKey 及其匹配 UIImage

保存文档缩略图

保存缩略图的最简单方法是使用 UIDocument。 通过调用 UIDocumentGetFileAttributesToWrite 方法并设置缩略图,将自动保存文档文件。 iCloud 守护程序将看到此更改并将其传播到 iCloud。 在 Mac OS X 上,通过快速查找插件为开发人员自动生成缩略图。

有了使用基于 iCloud 的文档的基础知识以及对现有 API 的修改,我们就可以在 Xamarin iOS 8 移动应用程序中实现文档选取器视图控制器。

在 Xamarin 中启用 iCloud

在 Xamarin.iOS 应用程序中使用文档选取器之前,需要在应用程序和通过 Apple 启用 iCloud 支持。

以下步骤演练了 iCloud 的预配过程。

  1. 创建 iCloud 容器。
  2. 创建包含 iCloud 应用服务的应用 ID。
  3. 创建包含此应用 ID 的预配配置文件。

使用功能指南介绍了前两个步骤。 若要创建预配配置文件,请按照预配配置文件指南中的步骤进行操作。

以下步骤演练了为 iCloud 配置应用程序的过程:

请执行以下操作:

  1. 在 Visual Studio for Mac 或 Visual Studio 中打开项目。

  2. 解决方案资源管理器中,右键单击项目并选择“选项”。

  3. 在“选项”对话框中,选择“iOS 应用程序”,确保“捆绑包标识符”与上面为应用程序创建的“应用 ID”中定义的标识符匹配。

  4. 选择“iOS 捆绑包签名”,然后选择上面创建的“开发人员标识”和“预配配置文件”。

  5. 单击“确定”按钮保存更改并关闭对话框。

  6. 右键单击解决方案资源管理器中的 Entitlements.plist,以在编辑器中打开它。

    重要

    在 Visual Studio 中,可能需要打开“权利”编辑器,方法是右键单击它,选择“打开方式…”,然后选择“属性列表编辑器”

  7. 检查“启用 iCloud”、“iCloud 文档”、“键值存储”和“CloudKit”。

  8. 确保应用程序存在容器(如上所述)。 示例: iCloud.com.your-company.AppName

  9. 保存对文件所做的更改。

有关权利的详细信息,请参阅使用权利指南。

完成上述设置后,应用程序现在可以使用基于云的文档和新的文档选取器视图控制器。

常用设置代码

在开始使用文档选取器视图控制器之前,需要一些标准设置代码。 首先修改应用程序的 AppDelegate.cs 文件,使其如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Foundation;
using UIKit;
using ObjCRuntime;
using System.IO;

namespace DocPicker
{

    [Register ("AppDelegate")]
    public partial class AppDelegate : UIApplicationDelegate
    {
        #region Static Properties
        public const string TestFilename = "test.txt";
        #endregion

        #region Computed Properties
        public override UIWindow Window { get; set; }
        public bool HasiCloud { get; set; }
        public bool CheckingForiCloud { get; set; }
        public NSUrl iCloudUrl { get; set; }

        public GenericTextDocument Document { get; set; }
        public NSMetadataQuery Query { get; set; }
        public NSData Bookmark { get; set; }
        #endregion

        #region Private Methods
        private void FindDocument () {
            Console.WriteLine ("Finding Document...");

            // Create a new query and set it's scope
            Query = new NSMetadataQuery();
            Query.SearchScopes = new NSObject [] {
                NSMetadataQuery.UbiquitousDocumentsScope,
                NSMetadataQuery.UbiquitousDataScope,
                NSMetadataQuery.AccessibleUbiquitousExternalDocumentsScope
            };

            // Build a predicate to locate the file by name and attach it to the query
            var pred = NSPredicate.FromFormat ("%K == %@",
                 new NSObject[] {NSMetadataQuery.ItemFSNameKey
                , new NSString(TestFilename)});
            Query.Predicate = pred;

            // Register a notification for when the query returns
            NSNotificationCenter.DefaultCenter.AddObserver (this
                , new Selector("queryDidFinishGathering:")
                , NSMetadataQuery.DidFinishGatheringNotification
                , Query);

            // Start looking for the file
            Query.StartQuery ();
            Console.WriteLine ("Querying: {0}", Query.IsGathering);
        }

        [Export("queryDidFinishGathering:")]
        public void DidFinishGathering (NSNotification notification) {
            Console.WriteLine ("Finish Gathering Documents.");

            // Access the query and stop it from running
            var query = (NSMetadataQuery)notification.Object;
            query.DisableUpdates();
            query.StopQuery();

            // Release the notification
            NSNotificationCenter.DefaultCenter.RemoveObserver (this
                , NSMetadataQuery.DidFinishGatheringNotification
                , query);

            // Load the document that the query returned
            LoadDocument(query);
        }

        private void LoadDocument (NSMetadataQuery query) {
            Console.WriteLine ("Loading Document...");    

            // Take action based on the returned record count
            switch (query.ResultCount) {
            case 0:
                // Create a new document
                CreateNewDocument ();
                break;
            case 1:
                // Gain access to the url and create a new document from
                // that instance
                NSMetadataItem item = (NSMetadataItem)query.ResultAtIndex (0);
                var url = (NSUrl)item.ValueForAttribute (NSMetadataQuery.ItemURLKey);

                // Load the document
                OpenDocument (url);
                break;
            default:
                // There has been an issue
                Console.WriteLine ("Issue: More than one document found...");
                break;
            }
        }
        #endregion

        #region Public Methods

        public void OpenDocument(NSUrl url) {

            Console.WriteLine ("Attempting to open: {0}", url);
            Document = new GenericTextDocument (url);

            // Open the document
            Document.Open ( (success) => {
                if (success) {
                    Console.WriteLine ("Document Opened");
                } else
                    Console.WriteLine ("Failed to Open Document");
            });

            // Inform caller
            RaiseDocumentLoaded (Document);
        }

        public void CreateNewDocument() {
            // Create path to new file
            // var docsFolder = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
            var docsFolder = Path.Combine(iCloudUrl.Path, "Documents");
            var docPath = Path.Combine (docsFolder, TestFilename);
            var ubiq = new NSUrl (docPath, false);

            // Create new document at path
            Console.WriteLine ("Creating Document at:" + ubiq.AbsoluteString);
            Document = new GenericTextDocument (ubiq);

            // Set the default value
            Document.Contents = "(default value)";

            // Save document to path
            Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForCreating, (saveSuccess) => {
                Console.WriteLine ("Save completion:" + saveSuccess);
                if (saveSuccess) {
                    Console.WriteLine ("Document Saved");
                } else {
                    Console.WriteLine ("Unable to Save Document");
                }
            });

            // Inform caller
            RaiseDocumentLoaded (Document);
        }

        /// <summary>
        /// Saves the document.
        /// </summary>
        /// <returns><c>true</c>, if document was saved, <c>false</c> otherwise.</returns>
        public bool SaveDocument() {
            bool successful = false;

            // Save document to path
            Document.Save (Document.FileUrl, UIDocumentSaveOperation.ForOverwriting, (saveSuccess) => {
                Console.WriteLine ("Save completion: " + saveSuccess);
                if (saveSuccess) {
                    Console.WriteLine ("Document Saved");
                    successful = true;
                } else {
                    Console.WriteLine ("Unable to Save Document");
                    successful=false;
                }
            });

            // Return results
            return successful;
        }
        #endregion

        #region Override Methods
        public override void FinishedLaunching (UIApplication application)
        {

            // Start a new thread to check and see if the user has iCloud
            // enabled.
            new Thread(new ThreadStart(() => {
                // Inform caller that we are checking for iCloud
                CheckingForiCloud = true;

                // Checks to see if the user of this device has iCloud
                // enabled
                var uburl = NSFileManager.DefaultManager.GetUrlForUbiquityContainer(null);

                // Connected to iCloud?
                if (uburl == null)
                {
                    // No, inform caller
                    HasiCloud = false;
                    iCloudUrl =null;
                    Console.WriteLine("Unable to connect to iCloud");
                    InvokeOnMainThread(()=>{
                        var okAlertController = UIAlertController.Create ("iCloud Not Available", "Developer, please check your Entitlements.plist, Bundle ID and Provisioning Profiles.", UIAlertControllerStyle.Alert);
                        okAlertController.AddAction (UIAlertAction.Create ("Ok", UIAlertActionStyle.Default, null));
                        Window.RootViewController.PresentViewController (okAlertController, true, null);
                    });
                }
                else
                {    
                    // Yes, inform caller and save location the Application Container
                    HasiCloud = true;
                    iCloudUrl = uburl;
                    Console.WriteLine("Connected to iCloud");

                    // If we have made the connection with iCloud, start looking for documents
                    InvokeOnMainThread(()=>{
                        // Search for the default document
                        FindDocument ();
                    });
                }

                // Inform caller that we are no longer looking for iCloud
                CheckingForiCloud = false;

            })).Start();

        }

        // This method is invoked when the application is about to move from active to inactive state.
        // OpenGL applications should use this method to pause.
        public override void OnResignActivation (UIApplication application)
        {
        }

        // This method should be used to release shared resources and it should store the application state.
        // If your application supports background execution this method is called instead of WillTerminate
        // when the user quits.
        public override void DidEnterBackground (UIApplication application)
        {
            // Trap all errors
            try {
                // Values to include in the bookmark packet
                var resources = new string[] {
                    NSUrl.FileSecurityKey,
                    NSUrl.ContentModificationDateKey,
                    NSUrl.FileResourceIdentifierKey,
                    NSUrl.FileResourceTypeKey,
                    NSUrl.LocalizedNameKey
                };

                // Create the bookmark
                NSError err;
                Bookmark = Document.FileUrl.CreateBookmarkData (NSUrlBookmarkCreationOptions.WithSecurityScope, resources, iCloudUrl, out err);

                // Was there an error?
                if (err != null) {
                    // Yes, report it
                    Console.WriteLine ("Error Creating Bookmark: {0}", err.LocalizedDescription);
                }
            }
            catch (Exception e) {
                // Report error
                Console.WriteLine ("Error: {0}", e.Message);
            }
        }

        // This method is called as part of the transition from background to active state.
        public override void WillEnterForeground (UIApplication application)
        {
            // Is there any bookmark data?
            if (Bookmark != null) {
                // Trap all errors
                try {
                    // Yes, attempt to restore it
                    bool isBookmarkStale;
                    NSError err;
                    var srcUrl = new NSUrl (Bookmark, NSUrlBookmarkResolutionOptions.WithSecurityScope, iCloudUrl, out isBookmarkStale, out err);

                    // Was there an error?
                    if (err != null) {
                        // Yes, report it
                        Console.WriteLine ("Error Loading Bookmark: {0}", err.LocalizedDescription);
                    } else {
                        // Load document from bookmark
                        OpenDocument (srcUrl);
                    }
                }
                catch (Exception e) {
                    // Report error
                    Console.WriteLine ("Error: {0}", e.Message);
                }
            }

        }

        // This method is called when the application is about to terminate. Save data, if needed.
        public override void WillTerminate (UIApplication application)
        {
        }
        #endregion

        #region Events
        public delegate void DocumentLoadedDelegate(GenericTextDocument document);
        public event DocumentLoadedDelegate DocumentLoaded;

        internal void RaiseDocumentLoaded(GenericTextDocument document) {
            // Inform caller
            if (this.DocumentLoaded != null) {
                this.DocumentLoaded (document);
            }
        }
        #endregion
    }
}

重要

上述代码包括上面的“发现和列出文档”部分中的代码。 正如它在实际应用程序中所显示的那样,它在这里完整地呈现。 为简单起见,此示例仅适用于单个硬编码文件 (test.txt)。

上面的代码公开了多个 iCloud 云盘快捷方式,使它们更易于在应用程序的其余部分使用。

接下来,将以下代码添加到将使用文档选取器或处理基于云的文档的任何视图或视图容器:

using CloudKit;
...

#region Computed Properties
/// <summary>
/// Returns the delegate of the current running application
/// </summary>
/// <value>The this app.</value>
public AppDelegate ThisApp {
    get { return (AppDelegate)UIApplication.SharedApplication.Delegate; }
}
#endregion

这会添加一个快捷方式以访问 AppDelegate 并访问上面创建的 iCloud 快捷方式。

有了此代码,我们来看看在 Xamarin iOS 8 应用程序中实现文档选取器视图控制器。

使用文档选取器视图控制器

在 iOS 8 之前,从另一个应用程序访问文档非常困难,因为无法从应用内发现应用程序外部的文档。

现有行为

现有行为概述

让我们来看看在 iOS 8 之前访问外部文档的情况:

  1. 首先,用户必须打开最初创建文档的应用程序。
  2. 选择“文档”,并使用 UIDocumentInteractionController 将文档发送到新的应用程序。
  3. 最后,原始文档的副本放置在新应用程序的容器中。

在此处,文档可供第二个应用程序打开和编辑。

发现应用容器外部的文档

在 iOS 8 中,应用程序能够轻松访问其自己的应用程序容器外部的文档:

发现应用容器外部的文档

使用新的 iCloud 文档选取器 (UIDocumentPickerViewController),iOS 应用程序可以直接在其应用程序容器外部发现和访问。 UIDocumentPickerViewController 提供了一种机制,使用户能够通过权限授予对这些已发现文档的访问权限和编辑这些文档。

应用程序必须选择加入,使其文档显示在 iCloud 文档选取器中,并可供其他应用程序发现和使用它们。 若要让 Xamarin iOS 8 应用程序共享其应用程序容器,请在标准文本编辑器中编辑其 Info.plist 文件,并将以下两行添加到字典底部(在 <dict>...</dict> 标记之间):

<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>

UIDocumentPickerViewController 提供了一个出色的新 UI,允许用户选择文档。 若要在 Xamarin iOS 8 应用程序中显示文档选取器视图控制器,请执行以下操作:

using MobileCoreServices;
...

// Allow the Document picker to select a range of document types
        var allowedUTIs = new string[] {
            UTType.UTF8PlainText,
            UTType.PlainText,
            UTType.RTF,
            UTType.PNG,
            UTType.Text,
            UTType.PDF,
            UTType.Image
        };

        // Display the picker
        //var picker = new UIDocumentPickerViewController (allowedUTIs, UIDocumentPickerMode.Open);
        var pickerMenu = new UIDocumentMenuViewController(allowedUTIs, UIDocumentPickerMode.Open);
        pickerMenu.DidPickDocumentPicker += (sender, args) => {

            // Wireup Document Picker
            args.DocumentPicker.DidPickDocument += (sndr, pArgs) => {

                // IMPORTANT! You must lock the security scope before you can
                // access this file
                var securityEnabled = pArgs.Url.StartAccessingSecurityScopedResource();

                // Open the document
                ThisApp.OpenDocument(pArgs.Url);

                // IMPORTANT! You must release the security lock established
                // above.
                pArgs.Url.StopAccessingSecurityScopedResource();
            };

            // Display the document picker
            PresentViewController(args.DocumentPicker,true,null);
        };

pickerMenu.ModalPresentationStyle = UIModalPresentationStyle.Popover;
PresentViewController(pickerMenu,true,null);
UIPopoverPresentationController presentationPopover = pickerMenu.PopoverPresentationController;
if (presentationPopover!=null) {
    presentationPopover.SourceView = this.View;
    presentationPopover.PermittedArrowDirections = UIPopoverArrowDirection.Down;
    presentationPopover.SourceRect = ((UIButton)s).Frame;
}

重要

开发人员必须先调用 NSUrlStartAccessingSecurityScopedResource 方法,然后才能访问外部文档。 必须调用 StopAccessingSecurityScopedResource 方法,以在加载文档后立即释放安全锁。

示例输出

下面是在 iPhone 设备上运行时上述代码如何显示文档选取器的示例:

  1. 用户启动应用程序并显示主界面:

    显示主接口

  2. 用户点击屏幕顶部的“操作”按钮,并要求从可用提供程序列表中选择“文档提供程序”:

    从可用的提供程序列表中选择一个文档提供程序

  3. 为所选文档提供程序显示文档选取器视图控制器

    显示文档选取器视图控制器

  4. 用户点击“文档文件夹”以显示其内容:

    文档文件夹内容

  5. 用户选择一个文档文档选取器关闭。

  6. 主界面重新显示,从外部容器加载文档并显示其内容。

文档选取器视图控制器的实际显示取决于用户安装在设备上的文档提供程序以及已实现哪个文档选取器模式。 上面的示例使用的是打开模式,下面将详细介绍其他模式类型。

管理外部文档

如上所述,在 iOS 8 之前,应用程序只能访问属于其应用程序容器一部分的文档。 在 iOS 8 中,应用程序可以从外部源访问文档:

管理外部文档概述

当用户从外部源中选择文档时,引用文档将写入指向原始文档的应用程序容器。

为了帮助将这一新功能添加到现有应用程序中,已向 NSMetadataQuery API 添加了多个新功能。 通常,应用程序使用“普遍文档范围”列出位于其应用程序容器中的文档。 使用此范围,将仅继续显示应用程序容器中的文档。

使用新的“普遍外部文档范围”将返回位于应用程序容器外部的文档,并返回它们的元数据。 NSMetadataItemUrlKey 将指向文档实际所在的 URL。

有时,应用程序不希望使用引用指向的文档。 相反,应用希望直接使用引用文档。 例如,应用可能需要在 UI 中的应用程序文件夹中显示文档,或者允许用户在文件夹内移动引用。

在 iOS 8 中,提供了一个新的 NSMetadataItemUrlInLocalContainerKey 来直接访问引用文档。 此键指向对应用程序容器中外部文档的实际引用。

NSMetadataUbiquitousItemIsExternalDocumentKey 用于测试文档是否在应用程序的容器外部。 NSMetadataUbiquitousItemContainerDisplayNameKey 用于访问容纳外部文档原始副本的容器的名称。

为什么需要文档引用

iOS 8 使用引用访问外部文档的主要原因是安全性。 没有向应用程序授予对任何其他应用程序的容器的访问权限。 只有文档选取器才能做到这一点,因为进程不足,并且具有系统范围的访问权限。

访问应用程序容器外部的文档的唯一方法是使用文档选取器,并且选取器返回的 URL 具有安全范围的。 安全范围 URL 包含的信息仅足以用于所选文档以及授予应用程序对文档的访问权限所需的限定范围的权限。

请务必注意,如果安全范围 URL 序列化为字符串,然后取消序列化,则安全信息将丢失,并且无法从 URL 访问文件。 文档引用功能提供了一种机制,用于返回到这些 URL 指向的文件。

因此,如果应用程序从其中一个引用文档获取 NSUrl,则它已附加了安全范围,并可用于访问该文件。 因此,强烈建议开发人员使用 UIDocument,因为它会为他们处理所有这些信息和过程。

使用书签

枚举应用程序的“文档”以返回到特定文档并不总是可行的,例如,在执行状态还原时。 iOS 8 提供了一种机制,用于创建直接面向给定文档的书签。

以下代码将从 UIDocumentFileUrl 属性创建书签:

// Trap all errors
try {
    // Values to include in the bookmark packet
    var resources = new string[] {
        NSUrl.FileSecurityKey,
        NSUrl.ContentModificationDateKey,
        NSUrl.FileResourceIdentifierKey,
        NSUrl.FileResourceTypeKey,
        NSUrl.LocalizedNameKey
    };

    // Create the bookmark
    NSError err;
    Bookmark = Document.FileUrl.CreateBookmarkData (NSUrlBookmarkCreationOptions.WithSecurityScope, resources, iCloudUrl, out err);

    // Was there an error?
    if (err != null) {
        // Yes, report it
        Console.WriteLine ("Error Creating Bookmark: {0}", err.LocalizedDescription);
    }
}
catch (Exception e) {
    // Report error
    Console.WriteLine ("Error: {0}", e.Message);
}

现有书签 API 用于根据现有 NSUrl 创建书签,可以保存和加载该书签来提供对外部文件的直接访问。 以下代码将还原在上面创建的书签:

if (Bookmark != null) {
    // Trap all errors
    try {
        // Yes, attempt to restore it
        bool isBookmarkStale;
        NSError err;
        var srcUrl = new NSUrl (Bookmark, NSUrlBookmarkResolutionOptions.WithSecurityScope, iCloudUrl, out isBookmarkStale, out err);

        // Was there an error?
        if (err != null) {
            // Yes, report it
            Console.WriteLine ("Error Loading Bookmark: {0}", err.LocalizedDescription);
        } else {
            // Load document from bookmark
            OpenDocument (srcUrl);
        }
    }
    catch (Exception e) {
        // Report error
        Console.WriteLine ("Error: {0}", e.Message);
    }
}

打开与导入模式以及文档选取器

文档选取器视图控制器具有两种不同的操作模式:

  1. 打开模式 – 在此模式下,当用户选择外部文档时,文档选取器将在应用程序容器中创建安全范围书签。

    应用程序容器中的安全范围书签

  2. 导入模式 – 在此模式下,当用户选择外部文档时,文档选取器不会创建书签,而是将文件复制到临时位置,并向应用程序提供对此位置的文档的访问权限:

    文档选取器会将文件复制到临时位置,并提供对位于此位置的文档的应用程序访问权限
    应用程序因任何原因终止后,将清空临时位置并删除文件。 如果应用程序需要维护对文件的访问权限,则应创建一个副本并将其放置在其应用程序容器中。

当应用程序希望与另一个应用程序协作并与该应用程序共享对文档所做的任何更改时,打开模式非常有用。 当应用程序不想与其他应用程序共享对文档的修改时,将使用导入模式。

将文档设置为外部文档

如上所述,iOS 8 应用程序无权访问其自己的应用程序容器外部的容器。 应用程序可以在本地或临时位置写入其自己的容器,然后使用特殊文档模式将生成的文档从应用程序容器外部移动到用户选择的位置。

若要将文档移动到外部位置,请执行以下操作:

  1. 首先在本地或临时位置创建新文档。
  2. 创建指向新文档的 NSUrl
  3. 打开新的文档选取器视图控制器,并使用 MoveToService 模式传递 NSUrl 它。
  4. 用户选择新位置后,文档将从当前位置移动到新位置。
  5. 引用文档将被写入应用的应用程序容器,以便创建应用程序仍可访问该文件。

以下代码可用于将文档移动到外部位置:var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.MoveToService);

上述过程返回的引用文档与文档选取器的打开模式创建的引用文档完全相同。 但是,有时,应用程序可能希望移动文档而不保留对文档的引用。

若要在不生成引用的情况下移动文档,请使用 ExportToService 模式。 示例: var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.ExportToService);

使用 ExportToService 模式时,文档将复制到外部容器,现有副本将保留在其原始位置。

文档提供程序扩展

借助 iOS 8,Apple 希望最终用户能够访问任何基于云的文档,无论它们实际存在于何处。 为了实现此目标,iOS 8 提供了新的文档提供程序扩展机制。

什么是文档提供程序扩展?

简单地说,文档提供程序扩展是开发人员或第三方向应用程序提供替代文档存储的一种方式,该存储可以以与现有 iCloud 存储位置完全相同的方式访问。

用户可以从文档选取器中选择这些备用存储位置之一,并且他们可以使用完全相同的访问模式(打开、导入、移动或导出)来处理该位置中的文件。

这是使用两个不同的扩展实现的:

  • 文档选取器扩展 – 提供一个 UIViewController 子类,该子类提供一个图形界面供用户从备用存储位置选择文档。 此子类将作为文档选取器视图控制器的一部分显示。
  • 文件提供扩展 – 这是一个非 UI 扩展,用于实际提供文件内容。 这些扩展通过文件协调 ( NSFileCoordinator ) 提供。 这是需要文件协调的另一个重要案例。

下图显示了使用文档提供程序扩展时的典型数据流:

此图显示了使用文档提供程序扩展时的典型数据流

将发生以下过程:

  1. 应用程序提供了文档选取器控制器,允许用户选择要处理的文件。
  2. 用户选择备用文件位置,并调用自定义 UIViewController 扩展来显示用户界面。
  3. 用户从此位置选择一个文件,然后 URL 被传递回文档选取器。
  4. 文档选取器选择文件的 URL,并将其返回给应用程序供用户处理。
  5. URL 将传递给文件协调器,以将文件内容返回到应用程序。
  6. 文件协调器调用自定义文件提供程序扩展以检索该文件。
  7. 文件的内容将返回给文件协调器。
  8. 文件的内容将返回给应用程序。

安全和书签

本部分将简要介绍通过书签进行的安全性和持久文件访问如何与文档提供程序扩展配合使用。 与自动将安全和书签保存到应用程序容器的 iCloud 文档提供程序不同,文档提供程序扩展不这样做,因为它们不是文档引用系统的一部分。

例如:在提供自己公司范围的安全数据存储的企业设置中,管理员不希望公共 iCloud 服务器访问或处理机密公司信息。 因此,无法使用内置文档引用系统。

书签系统仍然可以使用,文件提供程序扩展负责正确处理已添加书签的 URL 并返回其指向的内容。

出于安全考虑,iOS 8 具有一个隔离层,用于保留有关哪个应用程序有权访问哪个文件提供程序内的哪个标识符的信息。 需要注意的是,所有文件访问都由此隔离层控制。

下图显示了使用书签和文档提供程序扩展时的数据流:

此图显示了使用书签和文档提供程序扩展时的数据流

将发生以下过程:

  1. 应用程序即将进入后台,需要保留其状态。 它调用 NSUrl 来在备用存储中创建文件书签。
  2. NSUrl 调用文件提供程序扩展来获取文档的持久 URL。
  3. 文件提供程序扩展将 URL 作为字符串返回给 NSUrl
  4. NSUrl 将 URL 捆绑到书签中,并将其返回给应用程序。
  5. 当应用程序从后台唤醒并需要还原状态时,它会将书签传递给 NSUrl
  6. NSUrl 使用文件的 URL 调用文件提供程序扩展。
  7. 文件扩展提供程序访问该文件,并将文件的位置返回给 NSUrl
  8. 文件位置与安全信息捆绑在一起,并返回给应用程序。

从此处,应用程序可以访问该文件,并像平时一样使用它。

写入文件

本部分将简要介绍如何使用文档提供程序扩展将文件写入备用位置。 iOS 应用程序将使用文件协调来将信息保存到应用程序容器内的磁盘。 成功写入文件后不久,将向文件提供程序扩展通知所做的更改。

此时,文件提供程序扩展可以开始将文件上传到备用位置(或将文件标记为脏且需要上传)。

创建新的文档提供程序扩展

创建新文档提供程序扩展超出了本介绍性文章的范围。 此处提供此信息是为了表明,根据用户在 iOS 设备中加载的扩展,应用程序可能有权访问 Apple 提供的 iCloud 位置之外的文档存储位置。

开发人员在使用文档选取器和处理外部文档时应该意识到这一事实。 不应假定这些文档托管在 iCloud 中。

有关创建存储提供程序或文档选取器扩展的详细信息,请参阅应用扩展简介一文。

迁移到 iCloud 云盘

在 iOS 8 上,用户可以选择继续使用 iOS 7(及以前的系统)中使用的现有 iCloud 文档系统,也可以选择将现有文档迁移到新的 iCloud 云盘机制。

在 Mac OS X Yosemite 上,Apple 不提供后向兼容性,因此所有文档都必须迁移到 iCloud 云盘,否则它们将不再在设备之间更新。

将用户的帐户迁移到 iCloud 云盘后,只有使用 iCloud 云盘的设备才能在这些设备上传播对文档的更改。

重要

开发人员应注意,仅当用户的帐户已迁移到 iCloud 云盘时,本文中介绍的新功能才可用。

总结

本文介绍了对支持 iCloud 云盘和新的文档选取器视图控制器所需的现有 iCloud API 所做的更改。 它介绍了文件协调以及为什么在处理基于云的文档时它很重要。 它介绍了在 Xamarin.iOS 应用程序中启用基于云的文档所需的设置,并介绍了如何使用文档选取器视图控制器在应用的应用程序容器外部处理文档。

此外,本文还简要介绍了文档提供程序扩展,以及为什么开发人员在编写可以处理基于云的文档的应用程序时应该注意这些扩展。