Selector de documentos en Xamarin.iOS

El Selector de documentos permite compartir documentos entre aplicaciones. Estos documentos se pueden almacenar en iCloud o en un directorio de aplicación diferente. Los documentos se comparten a través del conjunto de extensiones de proveedor de documentosque el usuario ha instalado en su dispositivo.

Debido a la dificultad de mantener los documentos sincronizados entre aplicaciones y la nube, introducen una cierta cantidad de complejidad necesaria.

Requisitos

Se requiere lo siguiente para completar los pasos presentados en este artículo:

  • Xcode 7 e iOS 8 o posterior: las API de Xcode 7 e iOS 8 o más recientes de Apple deben instalarse y configurarse en el equipo del desarrollador.
  • Visual Studio o Visual Studio para Mac: se debe instalar la versión más reciente de Visual Studio para Mac.
  • Dispositivo iOS: un dispositivo iOS que ejecuta iOS 8 o superior.

Cambios en iCloud

Para implementar las nuevas características del Selector de documentos, se han realizado los siguientes cambios en el servicio iCloud de Apple:

  • El demonio de iCloud se ha reescrito completamente mediante CloudKit.
  • Las características existentes de iCloud ahora se llaman iCloud Drive.
  • Se ha agregado compatibilidad con el sistema operativo Microsoft Windows a iCloud.
  • Se ha agregado una carpeta de iCloud en el Finder de Mac OS.
  • Los dispositivos iOS pueden acceder al contenido de la carpeta Mac OS iCloud.

Importante

Apple proporciona herramientas para ayudar a los desarrolladores a tratar correctamente el Reglamento general de protección de datos (RGPD) de la Unión Europea.

¿Qué es un documento?

Al hacer referencia a un documento en iCloud, es una sola entidad independiente y debe percibirse como tal por parte del usuario. Es posible que un usuario quiera modificar el documento o compartirlo con otros usuarios (por ejemplo, mediante correo electrónico).

Hay varios tipos de archivos que el usuario reconocerá inmediatamente como documentos, por ejemplo, archivos de Páginas, de Keynote o de Números. Sin embargo, iCloud no se limita a este concepto. Por ejemplo, el estado de un juego (como un partido de Ajedrez) se puede tratar como un documento y almacenarse en iCloud. Este archivo podría pasarse entre los dispositivos de un usuario y permitirle retomar un juego en el estado en que lo dejó en un dispositivo diferente.

Tratar con documentos

Antes de profundizar en el código necesario para usar el selector de documentos con Xamarin, este artículo trata los procedimientos recomendados para trabajar con documentos de iCloud y varias de las modificaciones realizadas en las API existentes necesarias para admitir el selector de documentos.

Uso de la coordinación de archivos

Dado que un archivo se puede modificar desde varias ubicaciones diferentes, se debe usar la coordinación para evitar la pérdida de datos.

Using File Coordination

Demos un vistazo a la ilustración anterior:

  1. Un dispositivo iOS que usa la coordinación de archivos crea un nuevo documento y lo guarda en la carpeta de iCloud.
  2. iCloud guarda el archivo modificado en la nube para su distribución en todos los dispositivos.
  3. Un equipo Mac adjunto ve el archivo modificado en la carpeta de iCloud y usa la coordinación de archivos para copiar los cambios en el archivo.
  4. Un dispositivo que no usa la coordinación de archivos realiza un cambio en el archivo y lo guarda en la carpeta de iCloud. Estos cambios se replican instantáneamente en los otros dispositivos.

Supongamos que el dispositivo iOS original o el Mac estaba editando el archivo, ahora sus cambios se pierden y sobrescriben con la versión del archivo del dispositivo no coordinado. Para evitar la pérdida de datos, la coordinación de archivos es un elemento necesario al trabajar con documentos basados en la nube.

Uso de UIDocument

UIDocument simplifica las cosas (o NSDocument en macOS) haciendo todo el trabajo pesado para el desarrollador. Proporciona la coordinación de archivos integrada con colas en segundo plano para evitar que se bloquee la interfaz de usuario de la aplicación.

UIDocument expone varias API de alto nivel que facilitan el esfuerzo de desarrollar una aplicación de Xamarin para cualquier propósito que requiera el desarrollador.

El código siguiente crea una subclase de UIDocument para implementar un documento genérico basado en texto que se puede usar para almacenar y recuperar texto de 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
    }
}

La clase GenericTextDocument presentada anteriormente se usará en este artículo al trabajar con el selector de documentos y documentos externos en una aplicación de Xamarin.iOS 8.

Coordinación asincrónica de archivos

iOS 8 proporciona varias nuevas características asincrónicas de coordinación de archivos a través de las nuevas API de coordinación de archivos. Antes de iOS 8, todas las API de coordinación de archivos existentes eran totalmente sincrónicas. Esto significaba que el desarrollador era responsable de implementar su propia cola en segundo plano para impedir que la coordinación de archivos bloqueara la interfaz de usuario de la aplicación.

La nueva clase NSFileAccessIntent contiene una dirección URL que apunta al archivo y varias opciones para controlar el tipo de coordinación necesario. En el código siguiente se muestra cómo mover un archivo de una ubicación a otra mediante intenciones:

// 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);
    }
});

Descubrir y enumerar documentos

Descubrir y enumerar documentos se hace mediante las API existentes NSMetadataQuery. En esta sección se tratarán las nuevas características agregadas a NSMetadataQuery que hacen el trabajo con documentos aún más fácil que antes.

Comportamiento actual

Antes de iOS 8, NSMetadataQuery era lento para recoger los cambios de un archivo local, como elimina, crea y cambia el nombre.

NSMetadataQuery local file changes overview

En el diagrama anterior:

  1. En el caso de los archivos que ya existen en el contenedor de aplicaciones, NSMetadataQuery tiene registros NSMetadata creados previamente y agrupados para que estén disponibles instantáneamente en la aplicación.
  2. La aplicación crea un nuevo archivo en el contenedor de aplicaciones.
  3. Hay un retraso antes de que NSMetadataQuery vea la modificación en el contenedor de aplicaciones y cree el registro NSMetadata necesario.

Debido al retraso en la creación del registro NSMetadata, la aplicación debía tener dos orígenes de datos abiertos: uno para los cambios de archivo local y otro para los cambios basados en la nube.

Stitching

En iOS 8, NSMetadataQuery es más fácil de usar directamente con una nueva característica denominada Stitching:

NSMetadataQuery with a new feature called Stitching

Uso de Stitching en el diagrama anterior:

  1. Como antes, para los archivos que ya existen en el contenedor de aplicaciones, NSMetadataQuery tiene registros NSMetadata existentes creados previamente y agrupados.
  2. La aplicación crea un nuevo archivo en el contenedor de aplicaciones mediante la coordinación de archivos.
  3. Un enlace en el contenedor de aplicaciones ve la modificación y llama a NSMetadataQuery para crear el registro NSMetadata necesario.
  4. El registro NSMetadata se crea directamente después del archivo y está disponible para la aplicación.

Con el uso de Stitching la aplicación ya no tiene que abrir un origen de datos para supervisar los cambios de archivos locales y basados en la nube. Ahora la aplicación puede confiar en directamente en NSMetadataQuery.

Importante

Stitching solo funciona si la aplicación usa la coordinación de archivos como se muestra en la sección anterior. Si no se usa la coordinación de archivos, las API tienen el comportamiento predeterminado que existía antes de iOS 8.

Nuevas características de metadatos de iOS 8

Se han agregado las siguientes características nuevas a NSMetadataQuery en iOS 8:

  • NSMetatadataQuery ahora puede enumerar documentos no locales almacenados en la nube.
  • Se han agregado nuevas API para acceder a la información de metadatos en los documentos basados en la nube.
  • Hay una nueva API NSUrl_PromisedItems que tendrá acceso a los atributos de archivo de archivos que pueden tener o no su contenido disponible localmente.
  • Use el método GetPromisedItemResourceValue para obtener información sobre un archivo determinado o use el método GetPromisedItemResourceValues para obtener información sobre más de un archivo a la vez.

Se han agregado dos marcas de coordinación de archivos nuevas para tratar con metadatos:

  • NSFileCoordinatorReadImmediatelyAvailableMetadataOnly
  • NSFileCoordinatorWriteContentIndependentMetadataOnly

Con las marcas anteriores, no es necesario que el contenido del archivo de documento esté disponible localmente para que se usen.

El siguiente segmento de código muestra cómo usar NSMetadataQuery para consultar la existencia de un archivo específico y compilar el archivo si no existe:

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

Miniaturas de documento

Apple siente que la mejor experiencia de usuario al enumerar documentos para una aplicación es usar vistas previas. Esto proporciona contexto a los usuarios finales, para que puedan identificar rápidamente el documento con el que quieren trabajar.

Antes de iOS 8, mostrar las vistas previas del documento requería una implementación personalizada. Como novedad de iOS 8 se tienen los atributos del sistema de archivos, que permiten al desarrollador trabajar rápidamente con miniaturas de documentos.

Recuperación de miniaturas de documentos

Al llamar a los métodos GetPromisedItemResourceValue o GetPromisedItemResourceValues, se devuelve la API NSUrl_PromisedItems, un NSUrlThumbnailDictionary. La única clave que se encuentra actualmente en este diccionario es NSThumbnial1024X1024SizeKey y su coincidencia UIImage.

Guardar miniaturas de documento

La manera más fácil de guardar una miniatura es mediante UIDocument. Al llamar al método GetFileAttributesToWrite de UIDocument y establecer la miniatura, se guardará automáticamente cuando se guarde el archivo de documento. El demonio de iCloud verá este cambio y lo propagará a iCloud. En Mac OS X, el complemento Quick Look genera automáticamente las miniaturas para el desarrollador.

Teniendo los conceptos básicos del trabajo con documentos basados en iCloud, junto con las modificaciones en la API existente, estamos listos para implementar el controlador de vista del selector de documentos en una aplicación móvil de Xamarin iOS 8.

Habilitación de iCloud en Xamarin

Antes de usar el selector de documentos en una aplicación de Xamarin.iOS, la compatibilidad con iCloud debe habilitarse tanto en la aplicación como en Apple.

En los pasos siguientes se explica el proceso de aprovisionamiento para iCloud.

  1. Cree un contenedor de iCloud.
  2. Cree un identificador de aplicación que contenga iCloud App Service.
  3. Cree un perfil de aprovisionamiento que incluya este identificador de aplicación.

La guía Trabajar con capacidades explica los dos primeros pasos. Para crear un perfil de aprovisionamiento, siga los pasos descritos en la guía Perfil de aprovisionamiento.

En los pasos siguientes se explica el proceso de configurar la aplicación para iCloud:

Haga lo siguiente:

  1. Abra el proyecto en Visual Studio para Mac o Visual Studio.

  2. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Opciones.

  3. En el cuadro de diálogo Opciones, seleccione Aplicación iOS, asegúrese de que el identificador de agrupación coincide con el definido en el identificador de aplicación creado anteriormente para la aplicación.

  4. Seleccione Firma de paquetes de iOS, seleccione la Identidad del desarrollador y el Perfil de aprovisionamiento creado anteriormente.

  5. Haga clic en el botón Aceptar para guardar los cambios y cerrar el cuadro de diálogo.

  6. Haga clic con el botón derecho en Entitlements.plist en el Explorador de soluciones para abrirlo en el editor.

    Importante

    En Visual Studio, es posible que tenga que abrir el editor de derechos haciendo clic con el botón derecho, seleccionando Abrir con... y seleccionando Editor de lista de propiedades.

  7. Active Habilitar iCloud, Documentos de iCloud, Almacenamiento de valor clave y CloudKit.

  8. Asegúrese de que el contenedor existe para la aplicación (como se creó anteriormente). Ejemplo: iCloud.com.your-company.AppName

  9. Guarde los cambios en el archivo.

Para obtener más información sobre los derechos, consulte la guía Trabajar con derechos.

Con la configuración anterior establecida, la aplicación ahora puede usar documentos basados en la nube y el nuevo controlador de vista del selector de documentos.

Código de instalación común

Antes de empezar a trabajar con el controlador de vista del selector de documentos, se requiere cierto código de configuración estándar. Para empezar, modifique el archivo AppDelegate.cs de la aplicación y haga que tenga un aspecto similar al siguiente:

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
    }
}

Importante

El código anterior incluye el código de la sección Descubrir y enumerar documentos mencionada anteriormente. Se presenta aquí en su totalidad, como aparecería en una aplicación real. Por motivos de simplicidad, este ejemplo solo funciona con un único archivo codificado de forma rígida (test.txt).

El código anterior expone varios accesos directos de iCloud Drive para facilitar su trabajo con el resto de la aplicación.

A continuación, agregue el código siguiente a cualquier vista o contenedor de vistas que use el selector de documentos o trabaje con documentos basados en la nube:

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

Esto agrega un acceso directo para acceder a AppDelegate y a los accesos directos de iCloud creados anteriormente.

Con este código implementado, demos un vistazo a la implementación del controlador de vista del selector de documentos en una aplicación de Xamarin iOS 8.

Uso del controlador de vista del selector de documentos

Antes de iOS 8, era muy difícil acceder a Documentos desde otra aplicación porque no había ninguna manera de descubrir documentos fuera de la aplicación desde dentro de la aplicación.

Comportamiento actual

Existing Behavior overview

Demos un vistazo a cómo se accede a un documento externo antes de iOS 8:

  1. En primer lugar, el usuario tendría que abrir la aplicación que creó originalmente el documento.
  2. El documento está seleccionado y se usa UIDocumentInteractionController para enviar el documento a la nueva aplicación.
  3. Por último, se coloca una copia del documento original en el contenedor de la nueva aplicación.

Desde allí, el documento está disponible para que la segunda aplicación pueda abrir y editar.

Descubrir documentos fuera del contenedor de una aplicación

En iOS 8, una aplicación puede acceder a documentos fuera de su propio contenedor de aplicaciones con facilidad:

Discovering Documents Outside of an App's Container

Con el nuevo selector de documentos de iCloud (UIDocumentPickerViewController), una aplicación de iOS puede descubrir y acceder directamente fuera de su contenedor de aplicaciones. UIDocumentPickerViewController proporciona un mecanismo para que el usuario conceda acceso y edite los documentos detectados a través de permisos.

Una aplicación debe participar para que sus documentos aparezcan en el selector de documentos de iCloud y estén disponibles para que otras aplicaciones los detecten y trabajen con ellos. Para que una aplicación de Xamarin iOS 8 comparta su contenedor de aplicaciones, edite el archivo Info.plist en un editor de texto estándar y agregue las siguientes dos líneas a la parte inferior del diccionario (entre las etiquetas <dict>...</dict>):

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

UIDocumentPickerViewController proporciona una nueva interfaz de usuario excelente que permite al usuario elegir documentos. Para mostrar el controlador de vista del selector de documentos en una aplicación de Xamarin iOS 8, haga lo siguiente:

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;
}

Importante

El desarrollador debe llamar el método StartAccessingSecurityScopedResource de NSUrl antes de que se pueda acceder a un documento externo. Debe llamarse el método StopAccessingSecurityScopedResource para liberar el bloqueo de seguridad en cuanto se haya cargado el documento.

Salida de ejemplo

Este es un ejemplo de cómo el código anterior mostraría un selector de documentos cuando se ejecuta en un dispositivo iPhone:

  1. El usuario inicia la aplicación y se muestra la interfaz principal:

    The main interface is displayed

  2. El usuario pulsa el botón Acción en la parte superior de la pantalla y se le pide que seleccione un Proveedor de documentos de la lista de proveedores disponibles:

    Select a Document Provider from the list of available providers

  3. El Controlador de vista del selector de documentos se muestra para el proveedor de documentos seleccionado:

    The Document Picker View Controller is displayed

  4. El usuario pulsa en una carpeta de documentos para mostrar su contenido:

    The Document Folder contents

  5. El usuario selecciona un documento y se cierra el selector de documentos.

  6. La interfaz principal se vuelve a mostrar, el documento se carga desde el contenedor externo y se muestra su contenido.

La presentación real del controlador de vista del selector de documentos depende de los proveedores de documentos que el usuario ha instalado en el dispositivo y del modo del selector de documentos que se ha implementado. En el ejemplo anterior se usa el modo abierto, los otros tipos de modo se tratarán en detalle a continuación.

Administración de documentos externos

Como se explicó anteriormente, antes de iOS 8, una aplicación solo podía acceder a documentos que formaban parte de su contenedor de aplicaciones. En iOS 8, una aplicación puede acceder a documentos desde orígenes externos:

Managing External Documents overview

Cuando el usuario selecciona un documento de un origen externo, se escribe un documento de referencia en el contenedor de aplicaciones que apunta al documento original.

Para ayudar a agregar esta nueva capacidad a las aplicaciones existentes, se han agregado varias características nuevas a la API NSMetadataQuery. Normalmente, una aplicación usa el ámbito de documento omnipresente para enumerar los documentos que residen en su contenedor de aplicaciones. Con este ámbito, solo se mostrarán los documentos dentro del contenedor de aplicaciones.

El uso del nuevo ámbito de documento externo omnipresente devolverá documentos que residen fuera del contenedor de aplicaciones y devolverá sus metadatos. NSMetadataItemUrlKey apuntará a la dirección URL donde se encuentra realmente el documento.

A veces, una aplicación no quiere trabajar con los documentos a los que apunta la referencia. En su lugar, la aplicación quiere trabajar directamente con el documento de referencia. Por ejemplo, es posible que la aplicación quiera mostrar el documento en la carpeta de la aplicación en la interfaz de usuario o permitir que el usuario mueva las referencias dentro de una carpeta.

En iOS 8, se ha proporcionado un nuevo NSMetadataItemUrlInLocalContainerKey para acceder directamente al documento de referencia. Esta clave apunta a la referencia real al documento externo en un contenedor de aplicaciones.

NSMetadataUbiquitousItemIsExternalDocumentKey se usa para probar si un documento es externo al contenedor de una aplicación o no. NSMetadataUbiquitousItemContainerDisplayNameKey se usa para tener acceso al nombre del contenedor que contiene la copia original de un documento externo.

¿Por qué se requieren las referencias de documentos?

La razón principal por la que iOS 8 usa referencias para acceder a documentos externos es la seguridad. No se concede a ninguna aplicación acceso al contenedor de ninguna otra aplicación. Solo el Selector de documentos puede hacerlo, ya que se está ejecutando fuera de proceso y tiene acceso amplio al sistema.

La única manera de llegar a un documento fuera del contenedor de aplicaciones es mediante el Selector de documentos y si la dirección URL devuelta por el selector es del ámbito de seguridad. La dirección URL del ámbito de seguridad contiene información suficiente para seleccionar el documento junto con los derechos con ámbito necesarios para conceder acceso a una aplicación al documento.

Es importante tener en cuenta que si la dirección URL del ámbito de seguridad se serializó en una cadena y, a continuación, se dejó de serializar, la información de seguridad se perdería y el archivo no sería accesible desde la dirección URL. La característica de referencia de documento proporciona un mecanismo para volver a los archivos a los que apuntan estas direcciones URL.

Por lo tanto, si la aplicación adquiere un NSUrl de uno de los documentos de referencia, ya tiene el ámbito de seguridad asociado y se puede usar para acceder al archivo. Por este motivo, se recomienda encarecidamente que el desarrollador use UIDocument porque controla toda esta información y la procesa.

Utilizar marcadores

No siempre es factible enumerar los documentos de una aplicación para volver a un documento específico, por ejemplo, al realizar la restauración de estado. iOS 8 proporciona un mecanismo para crear marcadores que tienen como destino directamente un documento determinado.

El código siguiente creará un marcador a partir de una propiedad FileUrl de UIDocument:

// 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);
}

La API de marcador existente se usa para crear un marcador en un NSUrl existente que se puede guardar y cargar para proporcionar acceso directo a un archivo externo. El código siguiente restaurará un marcador que se creó anteriormente:

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);
    }
}

Abrir vs. Modo de importación y el selector de documentos

El controlador de vista del selector de documentos presenta dos modos de funcionamiento diferentes:

  1. Modo abierto: en este modo, cuando el usuario selecciona un documento externo, el selector de documentos creará un marcador con ámbito de seguridad en el contenedor de aplicaciones.

    A Security Scoped Bookmark in the Application Container

  2. Modo de importación: en este modo, cuando el usuario selecciona un documento externo, el selector de documentos no creará un marcador, sino que, en su lugar, copiará el archivo en una ubicación temporal y proporcionará a la aplicación acceso al documento en esta ubicación:

    The Document Picker will copy the file into a Temporary Location and provide the application access to the Document at this location
    Una vez que la aplicación finaliza por cualquier motivo, la ubicación temporal se vacía y se quita el archivo. Si la aplicación necesita mantener el acceso al archivo, debe realizar una copia y colocarla en su contenedor de aplicaciones.

El modo abierto es útil cuando la aplicación desea colaborar con otra aplicación y compartir los cambios realizados en el documento con esa aplicación. El modo de importación se usa cuando la aplicación no desea compartir sus modificaciones en un documento con otras aplicaciones.

Hacer que un documento sea externo

Como se indicó anteriormente, una aplicación de iOS 8 no tiene acceso a contenedores fuera de su propio contenedor de aplicaciones. La aplicación puede escribir en su propio contenedor localmente o en una ubicación temporal y, a continuación, usar un modo de documento especial para mover el documento resultante fuera del contenedor de aplicaciones a una ubicación elegida por el usuario.

Para mover un documento a una ubicación externa, haga lo siguiente:

  1. En primer lugar, cree un nuevo documento en una ubicación local o temporal.
  2. Cree un NSUrl que apunte al nuevo documento.
  3. Abra un nuevo controlador de vista del selector de documentos y pásele el NSUrl con el modo de MoveToService.
  4. Una vez que el usuario elija una nueva ubicación, el documento se moverá de su ubicación actual a la nueva ubicación.
  5. Se escribirá un documento de referencia en el contenedor de aplicaciones de la aplicación para que la aplicación de creación pueda acceder al archivo.

El código siguiente se puede usar para mover un documento a una ubicación externa: var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.MoveToService);

El documento de referencia devuelto por el proceso anterior es exactamente el mismo que el creado por el modo abierto del selector de documentos. Sin embargo, hay veces en que la aplicación podría querer mover un documento sin mantener la referencia a él.

Para mover un documento sin generar una referencia, use el modo ExportToService. Ejemplo: var picker = new UIDocumentPickerViewController (srcURL, UIDocumentPickerMode.ExportToService);

Cuando se usa el modo ExportToService, el documento se copia en el contenedor externo y la copia existente se deja en su ubicación original.

Extensiones del proveedor de documentos

Con iOS 8, Apple quiere que el usuario final pueda acceder a cualquiera de sus documentos basados en la nube, independientemente de dónde existan realmente. Para lograr este objetivo, iOS 8 proporciona un nuevo mecanismo de extensión del proveedor de documentos.

¿Qué es una extensión del proveedor de documentos?

En pocas palabras, una extensión del proveedor de documentos es la manera en que un desarrollador, o un tercero, proporciona a una aplicación un almacenamiento alternativo de documentos al que se pueda acceder exactamente igual que con la ubicación de almacenamiento de iCloud existente.

El usuario puede seleccionar una de estas ubicaciones de almacenamiento alternativas en el Selector de documentos y puede usar exactamente los mismos modos de acceso (abierto, importar, mover o exportar) para trabajar con archivos en esa ubicación.

Esto se implementa mediante dos extensiones diferentes:

  • Extensión del selector de documentos: proporciona una subclase UIViewController que ofrece una interfaz gráfica para que el usuario elija un documento de una ubicación de almacenamiento alternativa. Esta subclase se mostrará como parte del controlador de vista del selector de documentos.
  • Extensión para proporcionar archivos: se trata de una extensión que no es de interfaz de usuario y que se ocupa de proporcionar realmente el contenido de los archivos. Estas extensiones se proporcionan a través de la coordinación de archivos (NSFileCoordinator). Este es otro caso importante en el que se requiere la coordinación de archivos.

En el diagrama siguiente se muestra el flujo de datos típico al trabajar con extensiones del proveedor de documentos:

This diagram shows the typical data flow when working with Document Provider Extensions

Se produce el siguiente proceso:

  1. La aplicación presenta un controlador del selector de documentos para permitir al usuario seleccionar un archivo con el cual trabajar.
  2. El usuario selecciona una ubicación de archivo alternativa y se llama a la extensión personalizada UIViewController para mostrar la interfaz de usuario.
  3. El usuario selecciona un archivo de esta ubicación y la dirección URL se devuelve al selector de documentos.
  4. El Selector de documentos selecciona la dirección URL del archivo y la devuelve a la aplicación para que el usuario pueda trabajar en él.
  5. La dirección URL se pasa al coordinador de archivos para devolver el contenido de los archivos a la aplicación.
  6. El coordinador de archivos llama a la extensión de proveedor de archivos personalizada para recuperar el archivo.
  7. El contenido del archivo se devuelve al coordinador de archivos.
  8. El contenido del archivo se devuelve a la aplicación.

Seguridad y marcadores

En esta sección se examinará rápidamente cómo funciona la seguridad y el acceso persistente a los archivos a través de marcadores con extensiones del proveedor de documentos. A diferencia del proveedor de documentos de iCloud, que guarda automáticamente la seguridad y marcadores en el contenedor de aplicaciones, las extensiones del proveedor de documentos no lo hacen, ya que no forman parte del sistema de referencia de documentos.

Por ejemplo: en una configuración Enterprise que proporciona su propio almacén de datos seguro para toda la empresa, los administradores no quieren que se acceda a información corporativa confidencial o se procese por parte de los servidores públicos de iCloud. Por lo tanto, no se puede usar el sistema de referencia de documentos integrado.

El sistema de marcadores todavía se puede usar y es responsabilidad de la extensión del proveedor de archivos procesar correctamente una dirección URL con marcadores y devolver el contenido del documento al que apunta.

Con fines de seguridad, iOS 8 tiene una capa de aislamiento que conserva la información sobre qué aplicación tiene acceso a qué identificador dentro de qué proveedor de archivos. Debe tenerse en cuenta que todo el acceso a archivos se controla mediante esta capa de aislamiento.

En el diagrama siguiente se muestra el flujo de datos al trabajar con marcadores y una extensión del proveedor de documentos:

This diagram shows the data flow when working with Bookmarks and a Document Provider Extension

Se produce el siguiente proceso:

  1. La aplicación está a punto de quedar en segundo plano y necesita conservar su estado. Llama a NSUrl para crear un marcador en un archivo de un almacenamiento alternativo.
  2. NSUrl llama a la extensión del proveedor de archivos para obtener una dirección URL persistente al documento.
  3. La extensión del proveedor de archivos devuelve la dirección URL como una cadena para el NSUrl.
  4. NSUrl agrupa la dirección URL en un marcador y la devuelve a la aplicación.
  5. Cuando la aplicación se activa tras estar en segundo plano y necesita restaurar el estado, pasa el marcador a NSUrl.
  6. NSUrl llama a la extensión del proveedor de archivos con la dirección URL del archivo.
  7. El proveedor de extensión de archivo tiene acceso al archivo y devuelve la ubicación del archivo a NSUrl.
  8. La ubicación del archivo se agrupa con la información de seguridad y se devuelve a la aplicación.

Desde aquí, la aplicación puede acceder al archivo y trabajar con él normalmente.

Escribir archivos

En esta sección se examinará rápidamente cómo funciona escribir archivos en una ubicación alternativa con una extensión del proveedor de documentos. La aplicación iOS usará la coordinación de archivos para guardar información en el disco dentro del contenedor de aplicaciones. Poco después de que el archivo se haya escrito correctamente, la extensión del proveedor de archivos recibirá una notificación del cambio.

En este momento, la extensión del proveedor de archivos puede empezar a cargar el archivo en la ubicación alternativa (o marcar el archivo como sucio y requerir carga).

Crear nuevas extensiones del proveedor de documentos

La creación de nuevas extensiones de proveedor de documentos está fuera del ámbito de este artículo introductorio. Esta información se proporciona aquí para mostrar que, en función de las extensiones que un usuario ha cargado en su dispositivo iOS, una aplicación puede tener acceso a ubicaciones de almacenamiento de documentos fuera de la ubicación de iCloud proporcionada por Apple.

El desarrollador debe tener en cuenta este hecho al usar el Selector de documentos y trabajar con documentos externos. No debe asumir que esos documentos se hospedan en iCloud.

Para obtener más información sobre cómo crear un proveedor de almacenamiento o una extensión del selector de documentos, consulte el documento Introducción a las extensiones de aplicación.

Migración al iCloud Drive

En iOS 8, los usuarios pueden optar por seguir usando el sistema de documentos de iCloud existente usado en iOS 7 (y sistemas anteriores) o pueden optar por migrar documentos existentes al nuevo mecanismo de iCloud Drive.

En Mac OS X Yosemite, Apple no proporciona la compatibilidad con versiones anteriores, por lo que todos los documentos deben migrarse a iCloud Drive o ya no se actualizarán en todos los dispositivos.

Después de migrar la cuenta de un usuario a iCloud Drive, solo los dispositivos que usan iCloud Drive podrán propagar los cambios a los documentos en esos dispositivos.

Importante

Los desarrolladores deben tener en cuenta que las nuevas características que se tratan en este artículo solo están disponibles si la cuenta del usuario se ha migrado a iCloud Drive.

Resumen

En este artículo se han tratado los cambios en las API de iCloud existentes que son necesarias para admitir iCloud Drive y el nuevo controlador de vistas del selector de documentos. También se ha tratado la coordinación de archivos y por qué es importante al trabajar con documentos basados en la nube. Además, se ha abordado la configuración necesaria para habilitar documentos basados en la nube en una aplicación de Xamarin.iOS y se ha dado un vistazo introductorio a trabajar con documentos fuera del contenedor de aplicaciones de una aplicación mediante el controlador de vistas del selector de documentos.

Adicionalmente, en este artículo se han descrito brevemente las extensiones del proveedor de documentos y por qué el desarrollador debe conocerlas al escribir aplicaciones que pueden controlar documentos basados en la nube.