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 Drive。
  • 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 為開發人員執行所有繁重的工作,讓事情變得簡單(或在 NSDocument macOS上)。 它提供內建的檔案協調與背景佇列,以阻止應用程式的 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
    }
}

GenericTextDocument在 Xamarin.iOS 8 應用程式中使用檔案選擇器和外部檔時,本文中呈現的類別將會在整個文章中使用。

異步檔案協調

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. 在看到修改應用程式容器並建立所需的NSMetadata記錄之前NSMetadataQuery,會有延遲。

由於建立 NSMetadata 記錄的延遲,應用程式必須開啟兩個數據源:一個用於本機檔案變更,一個用於雲端式變更。

拼接

在 iOS 8 中, NSMetadataQuery 與稱為「縫合」的新功能直接搭配使用更容易:

NSMetadataQuery 與名為 Stitching 的新功能

使用上圖中的 [縫合] :

  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。 藉由呼叫 GetFileAttributesToWrite 的方法 UIDocument 並設定縮圖,它就會在檔案檔案為 時自動儲存。 iCloud 精靈會看到這項變更,並將其傳播至 iCloud。 在 Mac OS X 上,快速查看外掛程式會自動為開發人員產生縮圖。

有了使用 iCloud 型檔的基本概念,以及現有 API 的修改,我們即可在 Xamarin iOS 8 行動應用程式中實作檔選擇器檢視控制器。

在 Xamarin 中啟用 iCloud

在 Xamarin.iOS 應用程式中使用檔案選擇器之前,必須先在您的應用程式和 Apple 中啟用 iCloud 支援。

下列步驟逐步解說 iCloud 布建的程式。

  1. 建立 iCloud 容器。
  2. 建立包含 iCloud App Service 的應用程式識別碼。
  3. 建立包含此應用程式識別碼的布建配置檔。

使用功能指南會逐步解說前兩個步驟。 若要建立布建配置檔,請遵循布建配置檔指南中的步驟。

下列步驟逐步解說為 iCloud 設定應用程式的程式:

執行下列操作:

  1. 在 Visual Studio for Mac 或 Visual Studio 中開啟專案。

  2. 在 方案總管,以滑鼠右鍵按兩下項目,然後選取 [選項]。

  3. 在 [選項] 對話框中,選取 [iOS 應用程式],確定套件組合識別元符合針對應用程式建立的應用程式識別碼中所定義的識別符。

  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 Drive 快捷方式,使其更容易在應用程式的其餘部分使用。

接下來,將下列程式代碼新增至將使用文件選擇器或使用雲端式檔的任何檢視或檢視容器:

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 之前,很難從另一個應用程式存取 Documents,因為無法從應用程式內探索應用程式外部的檔。

現有行為

現有行為概觀

讓我們看看在 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;
}

重要

開發人員必須先呼叫 StartAccessingSecurityScopedResourceNSUrl 方法,才能存取外部檔。 StopAccessingSecurityScopedResource載入文件之後,必須呼叫 方法來釋放安全性鎖定。

範例輸出

以下是上述程式代碼如何在 i 電話 裝置上執行時顯示檔案選擇器的範例:

  1. 使用者啟動應用程式並顯示主要介面:

    主要介面隨即顯示

  2. 使用者點選 畫面頂端的 [動作 ] 按鈕,並要求從可用的提供者清單中選取 檔提供者

    從可用的提供者清單中選取檔案提供者

  3. 選取 的檔案選擇器檢視控制器 會顯示為選取 的檔案提供者

    檔選擇器檢視控制器隨即顯示

  4. 使用者點選 取 [檔案資料夾 ] 以顯示其內容:

    檔案資料夾內容

  5. 用戶選取檔關閉檔案選擇器

  6. 主要介面會重新執行, 會從外部容器載入,並顯示其內容。

檔選擇器檢視控制器的實際顯示取決於使用者已安裝在裝置上的檔提供者,以及已實作的檔選擇器模式。 上述範例使用開放模式,以下將詳細討論其他模式類型。

管理外部檔

如上所述,在iOS 8之前,應用程式只能存取屬於其應用程式容器一部分的檔。 在 iOS 8 中,應用程式可以從外部來源存取檔案:

管理外部檔概觀

當使用者從外部來源選取檔時,參考檔會寫入指向原始檔的應用程式容器。

為了協助將這項新的功能新增至現有的應用程式,已將數個新功能新增至 NSMetadataQuery API。 一般而言,應用程式會使用無處不在的檔範圍來列出其應用程式容器內的檔。 使用此範圍時,只會繼續顯示應用程式容器內的檔。

使用新的「無處不在的外部檔範圍」會傳回位於應用程式容器外部的檔,並傳回它們的元數據。 會 NSMetadataItemUrlKey 指向文件實際所在的URL。

有時候應用程式不想使用參考所指向的檔。 相反地,應用程式想要直接使用參考檔。 例如,應用程式可能想要在UI的Application資料夾中顯示檔,或允許使用者在資料夾內行動參考。

在 iOS 8 中,已提供新的 NSMetadataItemUrlInLocalContainerKey 來直接存取參考檔。 此索引鍵會指向應用程式容器中外部文件的實際參考。

NSMetadataUbiquitousItemIsExternalDocumentKey用來測試檔是否位於應用程式容器外部。 NSMetadataUbiquitousItemContainerDisplayNameKey用來存取容器的名稱,該容器是儲存外部檔的原始復本。

為何需要文件參考

iOS 8 使用參考來存取外部檔的主要原因是安全性。 沒有應用程式可存取任何其他應用程式的容器。 只有文件選擇器可以這樣做,因為已用盡進程且具有全系統存取權。

在應用程式容器外部取得檔的唯一方法是使用檔選擇器,而且選擇器傳回的 URL 是否為安全性範圍。 安全性範圍 URL 包含足夠的資訊來選取檔,以及授與應用程式存取檔所需的範圍許可權。

請務必注意,如果安全性範圍 URL 已串行化為字串串,然後取消串行化,則會遺失安全性資訊,且無法從 URL 存取檔案。 文件參考功能提供一種機制,可回到這些URL所指向的檔案。

因此,如果應用程式從其中一個參考檔取得 NSUrl ,它已經附加了安全性範圍,而且可以用來存取檔案。 基於這個理由,強烈建議開發人員使用 UIDocument ,因為它會處理所有這些資訊和程式。

使用書籤

列舉應用程式的檔不一定可行,以回到特定檔,例如,執行狀態還原時。 iOS 8 提供機制來建立直接以指定檔為目標的書籤。

下列程式代碼會從 UIDocumentFileUrl 屬性建立 Bookmark:

// 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 來建立 Bookmark,以提供外部檔案的直接存取權。 下列程式代碼會還原上面建立的書籤:

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 Mode。 範例: 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 檔提供者不同,檔提供者延伸模組不是文件參考系統的一部分。

例如:在提供自己全公司安全數據存放區的 Enterprise 設定中,系統管理員不希望公用 iCloud 伺服器存取或處理的機密公司資訊。 因此,無法使用內建的文件參考系統。

書籤系統仍可使用,而且檔案提供者延伸模組負責正確處理已加入書籤的URL,並傳回檔所指向的內容。

基於安全性考慮,iOS 8 具有隔離層,可保存哪些應用程式可存取檔案提供者內哪個標識碼的相關信息。 請注意,此隔離層會控制所有檔案存取。

下圖顯示使用書籤和檔案提供者延伸模組時的數據流:

此圖表顯示使用書籤和檔提供者延伸模組時的數據流

發生下列程式:

  1. 應用程式即將進入背景,且需要保存其狀態。 它會呼叫 NSUrl ,以在替代記憶體中建立檔案的書籤。
  2. NSUrl 會呼叫檔案提供者延伸模組,以取得文件的持續性URL。
  3. 檔案提供者擴展名會將 URL 當做字串傳回給 NSUrl
  4. 會將 NSUrl URL 組合成 Bookmark,並將它傳回給應用程式。
  5. 當應用程式從背景喚醒,且需要還原狀態時,它會將 Bookmark 傳遞至 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 Drive,否則它們將不再在裝置之間更新。

將使用者帳戶移轉至 iCloud 雲端硬碟之後,只有使用 iCloud Drive 的裝置才能將這些裝置上的變更傳播到檔。

重要

開發人員應該注意,本文中涵蓋的新功能只有在用戶帳戶已移轉至 iCloud Drive 時才可使用。

摘要

本文涵蓋支援 iCloud 磁碟驅動器和新的文件選擇器檢視控制器所需的現有 iCloud API 變更。 它涵蓋檔案協調,以及使用雲端式文件時為何很重要。 它涵蓋在 Xamarin.iOS 應用程式中啟用雲端式檔所需的設定,並提供使用檔案選擇器檢視控制器在應用程式應用程式容器外部使用檔的簡介外觀。

此外,本文簡要涵蓋檔提供者延伸模組,以及開發人員在撰寫可處理雲端式檔的應用程式時應該注意它們的原因。