Uso de iCloud con Xamarin.iOS

La API de almacenamiento de iCloud en iOS 5 permite a las aplicaciones guardar documentos de usuario y datos específicos de la aplicación en una ubicación central y acceder a esos elementos desde todos los dispositivos del usuario.

Hay cuatro tipos de almacenamiento disponibles:

  • Almacenamiento de clave-valor: para compartir pequeñas cantidades de datos con la aplicación en otros dispositivos de un usuario.

  • Almacenamiento UIDocument: para almacenar documentos y otros datos en la cuenta de iCloud del usuario mediante una subclase de UIDocument.

  • CoreData: almacenamiento de base de datos de SQLite.

  • Archivos y directorios individuales: para administrar muchos archivos diferentes directamente en el sistema de archivos.

En este documento se describen los dos primeros tipos: pares clave-valor y subclases UIDocument y cómo usar esas características en Xamarin.iOS.

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.

Requisitos

  • La versión estable más reciente de Xamarin.iOS
  • Xcode 10
  • Visual Studio para Mac o Visual Studio 2019.

Preparación para el desarrollo de iCloud

Las aplicaciones deben configurarse para usar iCloud tanto en el Portal de aprovisionamiento de Apple como en el propio proyecto. Antes de desarrollar para iCloud (o probar los ejemplos) siga estos pasos.

Para configurar correctamente una aplicación para acceder a iCloud:

  • Buscar el TeamID: inicie sesión en developer.apple.com y visite el Centro de miembros> Su cuenta > Resumen de la cuenta de desarrollador para obtener el identificador de equipo (o identificador individual para desarrolladores individuales). Será una cadena de 10 caracteres ( A93A5CM278 por ejemplo): forma parte del "identificador de contenedor".

  • Crear un nuevo identificador de aplicación: para crear un identificador de aplicación, siga los pasos descritos en la sección Aprovisionamiento para tecnologías de la tienda de la guía de Aprovisionamiento de dispositivos y asegúrese de seleccionar iCloud como servicio permitido:

Check iCloud as an allowed service

  • Crear un nuevo perfil de aprovisionamiento: para crear un perfil de aprovisionamiento, siga los pasos descritos en la Guía de aprovisionamiento de dispositivos.

  • Agregue el identificador de contenedor a Entitlements.plist: el formato de identificador de contenedor es TeamID.BundleID. Para obtener más información, consulte la guía Trabajar con derechos.

  • Configurar las propiedades del proyecto: en el archivo Info.plist, asegúrese de que el Identificador de agrupación coincide con el conjunto de Identificadores de lote al crear un identificador de aplicación; La firma de lote de iOS usa un Perfil de aprovisionamiento que contiene un identificador de aplicación con iCloud App Service y el archivo de Derechos personalizados seleccionado. Esto se puede hacer en Visual Studio en el panel Propiedades del proyecto.

  • Habilitar iCloud en el dispositivo: vaya a Configuración > iCloud y asegúrese de que el dispositivo haya iniciado sesión. Seleccione y active la opción Documentos y datos.

  • Debe usar un dispositivo para probar iCloud: no funcionará en el simulador. De hecho, realmente necesita dos o más dispositivos que hayan iniciado sesión con el mismo id. de Apple para ver iCloud en acción.

Almacenamiento de pares clave-valor

El almacenamiento de clave-valor está pensado para pequeñas cantidades de datos que un usuario podría desear conservar en todos los dispositivos, como la última página que vio en un libro o revista. El almacenamiento de clave-valor no debe usarse para realizar copias de seguridad de datos.

Hay algunas limitaciones que debe tener en cuenta al usar el almacenamiento de clave-valor:

  • Tamaño máximo de clave: los nombres de clave no pueden tener más de 64 bytes.

  • Tamaño máximo de valor: no se pueden almacenar más de 64 kilobytes en un solo valor.

  • Tamaño máximo del almacén de clave-valor para una aplicación: las aplicaciones solo pueden almacenar hasta 64 kilobytes de datos clave-valor en total. Se producirá un error en los intentos de establecer claves más allá de ese límite y el valor anterior se conservará.

  • Tipos de datos: solo se pueden almacenar tipos básicos, como cadenas, números y booleanos.

En el ejemplo de iCloudKeyValue se muestra cómo funciona. El código de ejemplo crea una clave denominada para cada dispositivo: puede establecer esta clave en un dispositivo y ver que el valor se propaga a otros usuarios. También crea una clave denominada "Compartido" que se puede editar en cualquier dispositivo; si edita en muchos dispositivos a la vez, iCloud decidirá qué valor "gana" (con una marca de tiempo en el cambio) y se propagará.

En esta captura de pantalla se muestra el ejemplo en uso. Cuando se reciben notificaciones de cambio de iCloud, se imprimen en la vista de texto de desplazamiento en la parte inferior de la pantalla y se actualizan en los campos de entrada.

The flow of messages between devices

Configuración y recuperación de datos

Este código muestra cómo establecer un valor de cadena.

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

Llamar a Synchronize garantiza que el valor solo se conserva en el almacenamiento en disco local. La sincronización con iCloud se produce en segundo plano y no se puede "forzar" mediante código de aplicación. Con una buena conectividad de red, la sincronización suele ocurrir en un plazo de 5 segundos, pero si la red es deficiente (o desconectada), una actualización puede tardar mucho más.

Puede recuperar un valor con este código:

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

El valor se recupera del almacén de datos local: este método no intenta ponerse en contacto con los servidores de iCloud para obtener el valor "más reciente". iCloud actualizará el almacén de datos local según su propia programación.

Eliminar datos

Para quitar completamente un par clave-valor, use el método Remove de la siguiente manera:

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

Observación de cambios

Una aplicación también puede recibir notificaciones cuando iCloud cambia los valores agregando un observador al NSNotificationCenter.DefaultCenter. El código siguiente del método KeyValueViewController.csViewWillAppear muestra cómo escuchar esas notificaciones y crear una lista de las claves que se han cambiado:

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

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

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

A continuación, el código puede realizar alguna acción con la lista de claves modificadas, como actualizar una copia local de ellas o actualizar la interfaz de usuario con los nuevos valores.

Los posibles motivos de cambio son: ServerChange (0), InitialSyncChange (1) o QuotaViolationChange (2). Puede acceder al motivo y realizar un procesamiento diferente si es necesario (por ejemplo, puede que tenga que quitar algunas claves como resultado de QuotaViolationChange).

Almacenamiento de documentos

El Almacenamiento de documentos de iCloud está diseñado para administrar los datos que son importantes para la aplicación (y para el usuario). Se puede usar para administrar archivos y otros datos que la aplicación necesita ejecutar, al mismo tiempo que proporciona funcionalidades de copia de seguridad y uso compartido basadas en iCloud en todos los dispositivos del usuario.

En este diagrama se muestra cómo encajan todos juntos. Cada dispositivo tiene datos guardados en el almacenamiento local (UbiquityContainer) y el demonio de iCloud del sistema operativo se encarga de enviar y recibir datos en la nube. Todo el acceso a archivos a UbiquityContainer debe realizarse a través de FilePresenter/FileCoordinator para evitar el acceso simultáneo. La clase UIDocument hace la implementación por usted. En este ejemplo se muestra cómo usar UIDocument.

The document storage overview

El ejemplo de iCloudUIDoc implementa una subclase simple UIDocument que contiene un único campo de texto. El texto se representa en una UITextView y las modificaciones se propagan por iCloud a otros dispositivos con un mensaje de notificación que se muestra en rojo. El código de ejemplo no se ocupa de características de iCloud más avanzadas, como la resolución de conflictos.

En esta captura de pantalla se muestra la aplicación de ejemplo: después de cambiar el texto y presionar UpdateChangeCount, el documento se sincroniza a través de iCloud con otros dispositivos.

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

Hay cinco partes en el ejemplo de iCloudUIDoc:

  1. Acceso a UbiquityContainer: determine si iCloud está habilitado y, si es así, la ruta de acceso al área de almacenamiento de iCloud de la aplicación.

  2. Creación de una subclase UIDocument: cree una clase para intermediar entre el almacenamiento de iCloud y los objetos de modelo.

  3. Buscar y abrir documentos de iCloud: use NSFileManager y NSPredicate para buscar documentos de iCloud y ábralos.

  4. Mostrar documentos de iCloud: exponga las propiedades de UIDocument para que pueda interactuar con controles de interfaz de usuario.

  5. Guardar documentos de iCloud: asegúrese de que los cambios realizados en la interfaz de usuario se conservan en el disco y en iCloud.

Todas las operaciones de iCloud se ejecutan (o deben ejecutarse) de forma asincrónica para que no se bloqueen mientras esperan a que suceda algo. Verá tres maneras diferentes de lograrlo en el ejemplo:

Subprocesos: en AppDelegate.FinishedLaunching la llamada inicial a GetUrlForUbiquityContainer se realiza en otro subproceso para evitar el bloqueo del subproceso principal.

NotificationCenter: registro para notificaciones cuando se completan las operaciones asincrónicas como NSMetadataQuery.StartQuery.

Controladores de finalización: pasar métodos para ejecutarse al completar operaciones asincrónicas como UIDocument.Open.

Acceso a UbiquityContainer

El primer paso para usar el Almacenamiento de documentos de iCloud es determinar si iCloud está habilitado y, si es así, la ubicación del "contenedor de ubiquity" (el directorio donde se almacenan los archivos habilitados para iCloud en el dispositivo).

Este código se encuentra en el método AppDelegate.FinishedLaunching del ejemplo.

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

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

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

Aunque el ejemplo no lo hace, Apple recomienda llamar a GetUrlForUbiquityContainer siempre que una aplicación llegue al primer plano.

Creación de una subclase UIDocument

Todos los archivos y directorios de iCloud (es decir, cualquier cosa almacenada en el directorio UbiquityContainer) deben administrarse mediante métodos NSFileManager, implementando el protocolo NSFilePresenter y escribiendo a través de un NSFileCoordinator. La manera más sencilla de hacer todo eso es no escribirlo usted mismo, sino que la subclase UIDocument lo hace por usted.

Solo hay dos métodos que debe implementar en una subclase UIDocument para trabajar con iCloud:

  • LoadFromContents: pasa el NSData del contenido del archivo para que se desempaquete en la clase o el modelo.

  • ContentsForType: solicitud para que proporcione la representación NSData de la clase o el modelo para guardarla en el disco (y en la nube).

Este código de ejemplo de iCloudUIDoc\MonkeyDocument.cs muestra cómo implementar UIDocument.

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

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

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

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

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

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

El modelo de datos en este caso es muy sencillo: un único campo de texto. El modelo de datos puede ser tan complejo como sea necesario, como un documento Xml o datos binarios. El rol principal de la implementación de UIDocument es traducir entre las clases de modelo y una representación NSData que se puede guardar o cargar en el disco.

Buscar y abrir documentos de iCloud

La aplicación de ejemplo solo se ocupa de un único archivo (test.txt), por lo que el código de AppDelegate.cs crea un NSPredicate y NSMetadataQuery para buscar específicamente ese nombre de archivo. La NSMetadataQuery se ejecuta de forma asincrónica y envía una notificación cuando finaliza. DidFinishGathering recibe la llamada del observador de notificaciones, detiene la consulta y llama a LoadDocument, que usa el método UIDocument.Open con un controlador de finalización para intentar cargar el archivo y mostrarlo en MonkeyDocumentViewController.

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

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

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

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

    query.StartQuery ();
}

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

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

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

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

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

Mostrar documentos de iCloud

Mostrar un UIDocument no debe ser diferente a ninguna otra clase de modelo: las propiedades se muestran en los controles de interfaz de usuario, posiblemente editados por el usuario y, a continuación, se escriben de nuevo en el modelo.

En el ejemplo iCloudUIDoc\MonkeyDocumentViewController.cs se muestra el texto MonkeyDocument en unaUITextView. ViewDidLoad escucha la notificación enviada en el método MonkeyDocument.LoadFromContents. LoadFromContents se llama cuando iCloud tiene nuevos datos para el archivo, de modo que la notificación indique que el documento se ha actualizado.

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

El controlador de notificaciones de código de ejemplo llama a un método para actualizar la interfaz de usuario, en este caso sin ninguna detección o resolución de conflictos.

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

Guardar documentos de iCloud

Para agregar un UIDocument a iCloud, puede llamar a UIDocument.Save directamente (solo para documentos nuevos) o mover un archivo existente mediante NSFileManager.DefaultManager.SetUbiquitious. El código de ejemplo crea un nuevo documento directamente en el contenedor de ubiquity con este código (hay dos controladores de finalización aquí, uno para la operación de Save y otro para open):

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

Los cambios posteriores en el documento no se "guardan" directamente, en su lugar se indica a UIDocument que ha cambiado con UpdateChangeCount y que programará automáticamente una operación de guardado en disco:

doc.UpdateChangeCount (UIDocumentChangeKind.Done);

Administración de documentos de iCloud

Los usuarios pueden administrar documentos de iCloud en el directorio Documentos del "contenedor de ubiquity" fuera de la aplicación a través de Configuración; pueden ver la lista de archivos y deslizar el dedo para eliminar. El código de aplicación debe ser capaz de controlar la situación en la que el usuario elimina los documentos. No almacene datos internos de la aplicación en el directorio Documentos.

Managing iCloud Documents workflow

Los usuarios también recibirán diferentes advertencias cuando intenten quitar una aplicación habilitada para iCloud de su dispositivo, para informarles del estado de los documentos de iCloud relacionados con esa aplicación.

Screenshot shows a warning for Document Updates Pending.

Screenshot shows a warning for Delete i Cloud.

Copia de seguridad de iCloud

Aunque la copia de seguridad en iCloud no es una característica a la que acceden directamente los desarrolladores, la forma en que diseña la aplicación puede afectar a la experiencia del usuario. Apple proporciona Instrucciones de almacenamiento de datos de iOS para que los desarrolladores sigan en sus aplicaciones de iOS.

La consideración más importante es si la aplicación almacena archivos grandes que no son generados por el usuario (por ejemplo, una aplicación de lector de revistas que almacena cientos de megabytes de contenido por problema). Apple prefiere que no almacene este tipo de datos en los que se realizará una copia de seguridad en iCloud y rellene innecesariamente la cuota de iCloud del usuario.

Las aplicaciones que almacenan grandes cantidades de datos como esta deben almacenarse en uno de los directorios de usuario de los que no se realiza una copia de seguridad (por ejemplo, Cachés o tmp) o usar NSFileManager.SetSkipBackupAttribute para aplicar una marca a esos archivos para que iCloud los ignore durante las operaciones de copia de seguridad.

Resumen

En este artículo se introdujo la nueva característica de iCloud incluida en iOS 5. Ha examinado los pasos necesarios para configurar el proyecto para usar iCloud y, a continuación, se proporcionan ejemplos de cómo implementar características de iCloud.

En el ejemplo de almacenamiento de clave-valor se muestra cómo se puede usar iCloud para almacenar una pequeña cantidad de datos similar a la forma en que se almacenan las NSUserPreferences. En el ejemplo UIDocument se muestra cómo se pueden almacenar y sincronizar datos más complejos entre varios dispositivos a través de iCloud.

Por último, se incluyó una breve explicación sobre cómo la adición de copia de seguridad de iCloud debe influir en el diseño de la aplicación.