Utilizar o iCloud com o Xamarin.iOS

A API de armazenamento do iCloud no iOS 5 permite que os aplicativos salvem documentos do usuário e dados específicos do aplicativo em um local central e acessem esses itens de todos os dispositivos do usuário.

Existem quatro tipos de armazenamento disponíveis:

  • Armazenamento de chave-valor - para compartilhar pequenas quantidades de dados com seu aplicativo em outros dispositivos de um usuário.

  • UIDocument storage - para armazenar documentos e outros dados na conta do iCloud do usuário usando uma subclasse de UIDocument.

  • CoreData - Armazenamento de banco de dados SQLite.

  • Arquivos e diretórios individuais - para gerenciar muitos arquivos diferentes diretamente no sistema de arquivos.

Este documento discute os dois primeiros tipos - pares Chave-Valor e subclasses UIDocument - e como usar esses recursos no Xamarin.iOS.

Importante

A Apple fornece ferramentas para ajudar os desenvolvedores a lidar adequadamente com o GDPR (Regulamento Geral sobre a Proteção de Dados) da União Europeia.

Requisitos

  • A última versão estável do Xamarin.iOS
  • Xcode 10
  • Visual Studio para Mac ou Visual Studio 2019.

Preparação para o desenvolvimento do iCloud

Os aplicativos devem ser configurados para usar o iCloud no Apple Provisioning Portal e no próprio projeto. Antes de desenvolver para o iCloud (ou experimentar os exemplos), siga os passos abaixo.

Para configurar corretamente um aplicativo para acessar o iCloud:

  • Encontre seu TeamID - faça login no developer.apple.com e visite o Member Center > Your Account Developer Account Account > Summary para obter seu Team ID (ou Individual ID para desenvolvedores individuais). Será uma cadeia de caracteres de 10 caracteres (A93A5CM278 por exemplo) - isso faz parte do "identificador de contêiner".

  • Criar uma nova ID de Aplicativo - Para criar uma ID de Aplicativo, siga as etapas descritas na seção Provisionamento para Tecnologias de Loja do guia de Provisionamento de Dispositivo e verifique o iCloud como um serviço permitido:

Check iCloud as an allowed service

  • Criar um novo Perfil de Provisionamento - Para criar um Perfil de Provisionamento, siga as etapas descritas no Guia de Provisionamento de Dispositivo.

  • Adicione o Identificador de Contêiner a Entitlements.plist - o formato do identificador de contêiner é TeamID.BundleID. Para obter mais informações, consulte o guia Trabalhando com direitos .

  • Configurar as propriedades do projeto - No arquivo Info.plist, verifique se o Identificador de Pacote corresponde ao ID do Pacote definido ao criar uma ID de Aplicativo; A Assinatura de Pacote do iOS usa um Perfil de Provisionamento que contém uma ID de Aplicativo com o Serviço de Aplicativo do iCloud e o arquivo de Direitos Personalizados selecionado. Isso tudo pode ser feito no Visual Studio no painel Propriedades do projeto.

  • Ative o iCloud no seu dispositivo - aceda a Definições > do iCloud e certifique-se de que o dispositivo tem sessão iniciada. Selecione e ative a opção Documentos e Dados .

  • Você deve usar um dispositivo para testar o iCloud - ele não funcionará no simulador. Na verdade, você realmente precisa de dois ou mais dispositivos conectados com o mesmo ID Apple para ver o iCloud em ação.

Armazenamento de chave-valor

O armazenamento de valor-chave destina-se a pequenas quantidades de dados que um usuário pode gostar que persistam em todos os dispositivos - como a última página que ele visualizou em um livro ou revista. O armazenamento de valor-chave não deve ser usado para fazer backup de dados.

Há algumas limitações a serem observadas ao usar o armazenamento de chave-valor:

  • Tamanho máximo da chave - Os nomes das chaves não podem ter mais de 64 bytes.

  • Tamanho máximo do valor - Não é possível armazenar mais de 64 kilobytes em um único valor.

  • Tamanho máximo do armazenamento de chave-valor para um aplicativo - Os aplicativos só podem armazenar até 64 kilobytes de dados de valor-chave no total. As tentativas de definir chaves além desse limite falharão e o valor anterior persistirá.

  • Tipos de dados - Somente tipos básicos como cadeias de caracteres, números e booleanos podem ser armazenados.

O exemplo iCloudKeyValue demonstra como ele funciona. O código de exemplo cria uma chave nomeada para cada dispositivo: você pode definir essa chave em um dispositivo e ver o valor ser propagado para outros. Ele também cria uma chave chamada "Compartilhado" que pode ser editada em qualquer dispositivo - se você editar em muitos dispositivos ao mesmo tempo, o iCloud decidirá qual valor "ganha" (usando um carimbo de data/hora na mudança) e será propagado.

Esta captura de tela mostra o exemplo em uso. Quando as notificações de alteração são recebidas do iCloud, elas são impressas na visualização de texto de rolagem na parte inferior da tela e atualizadas nos campos de entrada.

The flow of messages between devices

Configurando e recuperando dados

Este código mostra como definir um valor de cadeia de caracteres.

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

A Sincronização de Chamadas garante que o valor seja persistido somente para o armazenamento em disco local. A sincronização com o iCloud acontece em segundo plano e não pode ser "forçada" pelo código do aplicativo. Com uma boa conectividade de rede, a sincronização geralmente acontece dentro de 5 segundos, no entanto, se a rede estiver ruim (ou desconectada), uma atualização pode levar muito mais tempo.

Você pode recuperar um valor com este código:

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

O valor é recuperado do armazenamento de dados local - esse método não tenta entrar em contato com os servidores do iCloud para obter o valor "mais recente". O iCloud atualizará o armazenamento de dados local de acordo com sua própria programação.

Excluindo dados

Para remover completamente um par chave-valor, use o método Remove da seguinte maneira:

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

Observando mudanças

Um aplicativo também pode receber notificações quando os valores são alterados pelo iCloud, NSNotificationCenter.DefaultCenteradicionando um observador ao . O código a seguir do método KeyValueViewController.csViewWillAppear mostra como escutar essas notificações e criar uma lista de quais chaves foram alteradas:

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

Seu código pode então executar alguma ação com a lista de chaves alteradas, como atualizar uma cópia local delas ou atualizar a interface do usuário com os novos valores.

Os possíveis motivos de alteração são: ServerChange (0), InitialSyncChange (1) ou QuotaViolationChange (2). Você pode acessar o motivo e executar um processamento diferente, se necessário (por exemplo, talvez seja necessário remover algumas chaves como resultado de um QuotaViolationChange).

Armazenamento de documentos

O Armazenamento de Documentos do iCloud foi concebido para gerir dados importantes para a sua aplicação (e para o utilizador). Ele pode ser usado para gerenciar arquivos e outros dados que seu aplicativo precisa executar, ao mesmo tempo em que fornece backup baseado no iCloud e funcionalidade de compartilhamento em todos os dispositivos do usuário.

Este diagrama mostra como tudo se encaixa. Cada dispositivo tem dados salvos no armazenamento local (o UbiquityContainer) e o Daemon iCloud do sistema operacional se encarrega de enviar e receber dados na nuvem. Todo o acesso de arquivo ao UbiquityContainer deve ser feito via FilePresenter/FileCoordinator para impedir o acesso simultâneo. A UIDocument classe implementa esses para você, este exemplo mostra como usar UIDocument.

The document storage overview

O exemplo iCloudUIDoc implementa uma subclasse simples UIDocument que contém um único campo de texto. O texto é renderizado em um UITextView e as edições são propagadas pelo iCloud para outros dispositivos com uma mensagem de notificação mostrada em vermelho. O código de exemplo não lida com recursos mais avançados do iCloud, como resolução de conflitos.

Esta captura de tela mostra o aplicativo de exemplo - depois de alterar o texto e pressionar UpdateChangeCount o documento é sincronizado via iCloud para outros dispositivos.

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

Há cinco partes no exemplo do iCloudUIDoc:

  1. Acessando o UbiquityContainer - determine se o iCloud está habilitado e, em caso afirmativo, o caminho para a área de armazenamento do iCloud do seu aplicativo.

  2. Criando uma subclasse UIDocument - crie uma classe para intermediar entre o armazenamento do iCloud e seus objetos de modelo.

  3. Encontrar e abrir documentos do iCloud - utilize NSFileManager e encontre documentos do iCloud e NSPredicate abra-os.

  4. Exibindo documentos do iCloud - exponha as propriedades do seu UIDocument para que você possa interagir com os controles da interface do usuário.

  5. Salvar documentos do iCloud - certifique-se de que as alterações feitas na interface do usuário sejam persistentes no disco e no iCloud.

Todas as operações do iCloud são executadas (ou deveriam ser executadas) de forma assíncrona para que não sejam bloqueadas enquanto se espera que algo aconteça. Você verá três maneiras diferentes de fazer isso no exemplo:

Threads - na AppDelegate.FinishedLaunching chamada inicial para é feito em outro thread para GetUrlForUbiquityContainer evitar o bloqueio do thread principal.

NotificationCenter - registrando-se para receber notificações quando operações assíncronas forem NSMetadataQuery.StartQuery concluídas.

Manipuladores de conclusão - passando métodos para executar na conclusão de operações assíncronas como UIDocument.Open.

Acessando o UbiquityContainer

O primeiro passo para usar o Armazenamento de Documentos do iCloud é determinar se o iCloud está habilitado e, em caso afirmativo, a localização do "contêiner de ubiquidade" (o diretório onde os arquivos habilitados para o iCloud são armazenados no dispositivo).

Esse código está no AppDelegate.FinishedLaunching método do exemplo.

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

Embora o exemplo não faça isso, a Apple recomenda chamar GetUrlForUbiquityContainer sempre que um aplicativo vem à tona.

Criando uma subclasse UIDocument

Todos os arquivos e diretórios do iCloud (ou seja, qualquer coisa armazenada no diretório UbiquityContainer) devem ser gerenciados usando métodos NSFileManager, implementando o protocolo NSFilePresenter e escrevendo por meio de um NSFileCoordinator. A maneira mais simples de fazer tudo isso não é escrevê-lo sozinho, mas subclasse UIDocument que faz tudo por você.

Há apenas dois métodos que você deve implementar em uma subclasse UIDocument para trabalhar com o iCloud:

  • LoadFromContents - passa o NSData do conteúdo do arquivo para você descompactar em sua(s) classe(s) de modelo.

  • ContentsForType - solicitação para que você forneça a representação NSData de sua(s) classe(s) de modelo para salvar em disco (e na Nuvem).

Este código de exemplo de iCloudUIDoc\MonkeyDocument.cs mostra como 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;
    }
}

O modelo de dados, neste caso, é muito simples - um único campo de texto. Seu modelo de dados pode ser tão complexo quanto necessário, como um documento Xml ou dados binários. A função principal da implementação do UIDocument é traduzir entre suas classes de modelo e uma representação NSData que pode ser salva/carregada no disco.

Localizando e abrindo documentos do iCloud

O aplicativo de exemplo lida apenas com um único arquivo - teste.txt - para que o código em AppDelegate.cs crie um NSPredicate e NSMetadataQuery procure especificamente esse nome de arquivo. O NSMetadataQuery é executado de forma assíncrona e envia uma notificação quando termina. DidFinishGathering é chamado pelo observador de notificação, interrompe a consulta e chama LoadDocument, que usa o método com um manipulador de conclusão para tentar carregar o UIDocument.Open arquivo e exibi-lo em um MonkeyDocumentViewControllerarquivo .

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
}

Exibindo documentos do iCloud

A exibição de um UIDocument não deve ser diferente de qualquer outra classe de modelo - as propriedades são exibidas em controles de interface do usuário, possivelmente editadas pelo usuário e, em seguida, gravadas de volta no modelo.

No exemplo iCloudUIDoc\MonkeyDocumentViewController.cs exibe o texto MonkeyDocument em um UITextViewarquivo . ViewDidLoad escuta a notificação enviada no MonkeyDocument.LoadFromContents método. LoadFromContents é chamado quando o iCloud tem novos dados para o arquivo, para que a notificação indique que o documento foi atualizado.

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

O manipulador de notificação de código de exemplo chama um método para atualizar a interface do usuário - neste caso, sem qualquer detecção ou resolução de conflito.

[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 do iCloud

Para adicionar um UIDocument ao iCloud, você pode ligar UIDocument.Save diretamente (apenas para novos documentos) ou mover um arquivo existente usando NSFileManager.DefaultManager.SetUbiquitiouso . O código de exemplo cria um novo documento diretamente no contêiner de ubiquidade com esse código (há dois manipuladores de conclusão aqui, um para a Save operação e outro para o 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");
}

As alterações subsequentes no documento não são "salvas" diretamente, em vez disso, informamos que ele foi alterado com UpdateChangeCounto UIDocument , e ele agendará automaticamente uma operação de salvamento em disco:

doc.UpdateChangeCount (UIDocumentChangeKind.Done);

Gerir documentos do iCloud

Os usuários podem gerenciar documentos do iCloud no diretório Documentos do "contêiner de ubiquidade" fora do seu aplicativo por meio de Configurações, eles podem visualizar a lista de arquivos e deslizar para excluir. O código do aplicativo deve ser capaz de lidar com a situação em que os documentos são excluídos pelo usuário. Não armazene dados internos do aplicativo no diretório Documentos .

Managing iCloud Documents workflow

Os usuários também receberão avisos diferentes quando tentarem remover um aplicativo habilitado para iCloud de seu dispositivo, para informá-los sobre o status dos documentos do iCloud relacionados a esse aplicativo.

Screenshot shows a warning for Document Updates Pending.

Screenshot shows a warning for Delete i Cloud.

Backup do iCloud

Embora o backup no iCloud não seja um recurso acessado diretamente pelos desenvolvedores, a maneira como você projeta seu aplicativo pode afetar a experiência do usuário. A Apple fornece diretrizes de armazenamento de dados do iOS para os desenvolvedores seguirem em seus aplicativos iOS.

A consideração mais importante é se seu aplicativo armazena arquivos grandes que não são gerados pelo usuário (por exemplo, um aplicativo leitor de revista que armazena mais de cem megabytes de conteúdo por edição). A Apple prefere que você não armazene esse tipo de dado onde ele será copiado para o iCloud e preencherá desnecessariamente a cota do iCloud do usuário.

Aplicativos que armazenam grandes quantidades de dados como este devem armazená-los em um dos diretórios de usuário que não é feito backup (por exemplo. Caches ou tmp) ou use NSFileManager.SetSkipBackupAttribute para aplicar um sinalizador a esses arquivos para que o iCloud os ignore durante as operações de backup.

Resumo

Este artigo apresentou a nova funcionalidade do iCloud incluída no iOS 5. Ele examinou as etapas necessárias para configurar seu projeto para usar o iCloud e, em seguida, forneceu exemplos de como implementar os recursos do iCloud.

O exemplo de armazenamento de chave-valor demonstrou como o iCloud pode ser usado para armazenar uma pequena quantidade de dados semelhante à maneira como NSUserPreferences são armazenados. O exemplo UIDocument mostrou como dados mais complexos podem ser armazenados e sincronizados em vários dispositivos via iCloud.

Finalmente, incluiu uma breve discussão sobre como a adição do Backup do iCloud deve influenciar o design do seu aplicativo.