Noções básicas do iCloud e amostra de código [fechado]

86

Como um iniciante, estou lutando com o iCloud. Existem alguns exemplos, mas eles geralmente são bastante detalhados (no fórum de desenvolvedores há um para iCloud e CoreData que é enorme). Os documentos da apple estão OK, mas ainda não consigo ver o quadro geral. Então, por favor, tenha paciência comigo, algumas dessas perguntas são bastante fundamentais, mas possivelmente fáceis de responder.

Contexto: Tenho um aplicativo iCloud muito simples em execução (código de exemplo completo abaixo). Existe apenas um UITextView mostrado ao usuário e sua entrada é salva em um arquivo chamado text.txt.

insira a descrição da imagem aqui

O arquivo txt é enviado para a nuvem e disponibilizado para todos os dispositivos. Funciona perfeitamente, mas:

Principal problema: E quanto aos usuários que não usam o iCloud?

Quando eu inicio meu aplicativo (veja o código abaixo), eu verifico se o usuário tem o iCloud habilitado. Se o iCloud estiver ativado, está tudo bem. O aplicativo segue em frente e procura por text.txt na nuvem. Se encontrado, ele irá carregá-lo e exibi-lo para o usuário. Se text.txt não for encontrado na nuvem, ele simplesmente criará um novo text.txt e o exibirá ao usuário.

Se o usuário não tiver o iCloud habilitado, nada acontecerá. Como possibilitarei que usuários não-iCloud ainda possam trabalhar com meu aplicativo de texto? Ou simplesmente os ignoro? Eu precisaria escrever funções separadas para usuários que não são do iCloud? Ou seja, funções nas quais eu simplesmente carrego um text.txt da pasta de documentos?

Apple escreve :

Trate os arquivos no iCloud da mesma maneira que trata todos os outros arquivos na caixa de proteção do aplicativo.

No entanto, no meu caso, não há mais sandbox de aplicativo 'normal'. Está na nuvem. Ou sempre carrego primeiro meu text.txt do disco e depois verifico com o iCloud se há algo mais atualizado?

Problema relacionado: Estrutura de arquivos - Sandbox vs. Nuvem

Talvez meu principal problema seja um mal-entendido fundamental sobre como o iCloud deve funcionar. Quando eu crio uma nova instância de um UIDocument, terei que substituir dois métodos. Primeiro, - (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outErrorpara obter arquivos da nuvem e, em seguida, -(id)contentsForType:(NSString *)typeName error:(NSError **)outErrorpara obter arquivos para a nuvem.

Devo incorporar funções separadas que também salvam uma cópia local do text.txt em minha sandbox? Isso funcionará para usuários que não são do iCloud? Pelo que entendi, o iCloud salvará uma cópia local do text.txt automaticamente. Portanto, não deve haver nenhuma necessidade de eu salvar nada na 'velha' caixa de areia do meu aplicativo (ou seja, como costumava ser nos dias anteriores ao iCloud). No momento, minha sandbox está totalmente vazia, mas não sei se isso está correto. Devo manter outra cópia do text.txt lá? Isso parece bagunçar minha estrutura de dados ... já que há um text.txt na nuvem, um na sandbox do iCloud no meu dispositivo (que funcionará mesmo se eu estiver offline) e um terceiro na boa e velha sandbox do meu app ...


MEU CÓDIGO: Um código de exemplo simples do iCloud

Isso é vagamente baseado em um exemplo que encontrei no fórum de desenvolvedores e no vídeo da sessão WWDC. Eu reduzi ao mínimo. Não tenho certeza se minha estrutura MVC é boa. O modelo está no AppDelegate, o que não é o ideal. Quaisquer sugestões para torná-lo melhor são bem-vindas.


EDIT: Tentei extrair a pergunta principal e postei-a [aqui]. 4


VISÃO GERAL:

Visão geral

A parte mais importante que carrega o text.txt da nuvem:

//  AppDelegate.h
//  iCloudText

#import <UIKit/UIKit.h>

@class ViewController;
@class MyTextDocument;

@interface AppDelegate : UIResponder <UIApplicationDelegate> {
    NSMetadataQuery *_query;
}

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@property (strong, nonatomic) MyTextDocument *document;

@end

//  AppDelegate.m
//  iCloudText

#import "AppDelegate.h"
#import "MyTextDocument.h"
#import "ViewController.h"

@implementation AppDelegate

@synthesize window = _window;
@synthesize viewController = _viewController;
@synthesize document = _document;

- (void)dealloc
{
    [_window release];
    [_viewController release];
    [super dealloc];
}

- (void)loadData:(NSMetadataQuery *)query {

    // (4) iCloud: the heart of the load mechanism: if texts was found, open it and put it into _document; if not create it an then put it into _document

    if ([query resultCount] == 1) {
        // found the file in iCloud
        NSMetadataItem *item = [query resultAtIndex:0];
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:url];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc openWithCompletionHandler:^(BOOL success) {
            if (success) {
                NSLog(@"AppDelegate: existing document opened from iCloud");
            } else {
                NSLog(@"AppDelegate: existing document failed to open from iCloud");
            }
        }];
    } else {
        // Nothing in iCloud: create a container for file and give it URL
        NSLog(@"AppDelegate: ocument not found in iCloud.");

        NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
        NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:@"Documents"] URLByAppendingPathComponent:@"text.txt"];

        MyTextDocument *doc = [[MyTextDocument alloc] initWithFileURL:ubiquitousPackage];
        //_document = doc;
        doc.delegate = self.viewController;
        self.viewController.document = doc;

        [doc saveToURL:[doc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            NSLog(@"AppDelegate: new document save to iCloud");
            [doc openWithCompletionHandler:^(BOOL success) {
                NSLog(@"AppDelegate: new document opened from iCloud");
            }];
        }];
    }
}

- (void)queryDidFinishGathering:(NSNotification *)notification {

    // (3) if Query is finished, this will send the result (i.e. either it found our text.dat or it didn't) to the next function

    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [self loadData:query];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];
    _query = nil; // we're done with it
}

-(void)loadDocument {

    // (2) iCloud query: Looks if there exists a file called text.txt in the cloud

    NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
    _query = query;
    //SCOPE
    [query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
    //PREDICATE
    NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K == %@", NSMetadataItemFSNameKey, @"text.txt"];
    [query setPredicate:pred];
    //FINISHED?
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(queryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
    [query startQuery];

}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"AppDelegate: app did finish launching");
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];

    // Override point for customization after application launch.
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPhone" bundle:nil] autorelease];
    } else {
        self.viewController = [[[ViewController alloc] initWithNibName:@"ViewController_iPad" bundle:nil] autorelease];
    }

    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];

    // (1) iCloud: init

    NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    if (ubiq) {
        NSLog(@"AppDelegate: iCloud access!");
        [self loadDocument];
    } else {
        NSLog(@"AppDelegate: No iCloud access (either you are using simulator or, if you are on your phone, you should check settings");
    }


    return YES;
}

@end

O UIDocument

//  MyTextDocument.h
//  iCloudText

#import <Foundation/Foundation.h>
#import "ViewController.h"

@interface MyTextDocument : UIDocument {

    NSString *documentText;
    id delegate;

}

@property (nonatomic, retain) NSString *documentText;
@property (nonatomic, assign) id delegate;

@end

//  MyTextDocument.m
//  iCloudText

#import "MyTextDocument.h"
#import "ViewController.h"

@implementation MyTextDocument

@synthesize documentText = _text;
@synthesize delegate = _delegate;

// ** READING **

- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError
{
    NSLog(@"UIDocument: loadFromContents: state = %d, typeName=%@", self.documentState, typeName);

    if ([contents length] > 0) {
        self.documentText = [[NSString alloc] initWithBytes:[contents bytes] length:[contents length] encoding:NSUTF8StringEncoding];
    }
    else {
        self.documentText = @"";
    }

    NSLog(@"UIDocument: Loaded the following text from the cloud: %@", self.documentText);


    // update textView in delegate...
    if ([_delegate respondsToSelector:@selector(noteDocumentContentsUpdated:)]) {
        [_delegate noteDocumentContentsUpdated:self];
    }

    return YES;

}

// ** WRITING **

-(id)contentsForType:(NSString *)typeName error:(NSError **)outError
{
    if ([self.documentText length] == 0) {
        self.documentText = @"New Note";
    }

    NSLog(@"UIDocument: Will save the following text in the cloud: %@", self.documentText);

    return [NSData dataWithBytes:[self.documentText UTF8String] length:[self.documentText length]];
}
@end

O VIEWCONTROLLER

//
//  ViewController.h
//  iCloudText

#import <UIKit/UIKit.h>

@class MyTextDocument;

@interface ViewController : UIViewController <UITextViewDelegate> {

    IBOutlet UITextView *textView;

}

@property (nonatomic, retain) UITextView *textView;
@property (strong, nonatomic) MyTextDocument *document;

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument;

@end

//  ViewController.m
//  iCloudText

#import "ViewController.h"
#import "MyTextDocument.h"

@implementation ViewController

@synthesize textView = _textView;
@synthesize document = _document;

-(IBAction)dismissKeyboard:(id)sender {

    [_textView resignFirstResponder];

}

-(void)noteDocumentContentsUpdated:(MyTextDocument *)noteDocument
{
    NSLog(@"VC: noteDocumentsUpdated");
    _textView.text = noteDocument.documentText;
}

-(void)textViewDidChange:(UITextView *)theTextView {

     NSLog(@"VC: textViewDidChange");
    _document.documentText = theTextView.text;
    [_document updateChangeCount:UIDocumentChangeDone];

}
deixa pra lá
fonte
4
Eu realmente sugiro dividir isso em algumas perguntas. Vejo algumas questões diferentes enterradas aqui e é difícil identificá-las na parede de texto que você tem aqui. Eu voltaria a perguntar sobre o que fazer para as pessoas que não têm o iCloud habilitado e dividiria as outras (apenas com as partes relevantes de seu código de amostra) em perguntas separadas. São boas perguntas, mas acho que deveriam ser divididas.
Brad Larson
@BradLarson Obrigado pelo seu comentário. Lamento se as perguntas estiverem um pouco confusas, mas acho que a questão principal (como tentei indicar) é o problema do sandbox do aplicativo vs. sandbox do iCloud. Eu forneci o código completo (que é o menor exemplo de código do iCloud, aliás), pois pensei que o contexto INTEIRO é vital para saber o que está acontecendo ... Mas posso apenas abrir outra pergunta e vinculá-la de volta a esta pergunta para obter a imagem maior.
n.evermind,
@BradLarson OK, abri uma nova pergunta aqui: stackoverflow.com/questions/7798555/…
n.evermind
Para aqueles que ainda estão tentando se familiarizar
Duncan Groenewald
Não deveria ser fechado, esta é uma das postagens mais construtivas que eu vi no iCloud.
Andrew Smith

Respostas:

22

Acabei de reler os documentos e parece que minha abordagem geral está errada. Devo primeiro criar o arquivo na sandbox e depois movê-lo para a nuvem. Em outras palavras, a Apple parece sugerir que eu deveria ter três versões do mesmo arquivo em todos os momentos: uma no diretório do meu aplicativo, uma no diretório demon iCloud do meu dispositivo (que também pode ser acessado off-line) e uma em a nuvem:

Os aplicativos usam as mesmas tecnologias para gerenciar arquivos e diretórios no iCloud que usam para arquivos e diretórios locais. Arquivos e diretórios no iCloud ainda são apenas arquivos e diretórios. Você pode abri-los, criá-los, movê-los, copiá-los, ler e escrever neles, excluí-los ou qualquer outra operação que desejar. A única diferença entre arquivos e diretórios locais e arquivos e diretórios do iCloud é a URL que você usa para acessá-los. Em vez de os URLs serem relativos à caixa de proteção do seu aplicativo, os URLs dos arquivos e diretórios do iCloud são relativos ao diretório de contêiner do iCloud correspondente.

Para mover um arquivo ou diretório para o iCloud:

Crie o arquivo ou diretório localmente na caixa de proteção do seu aplicativo. Enquanto estiver em uso, o arquivo ou diretório deve ser gerenciado por um apresentador de arquivo, como um objeto UIDocument.

Use o método URLForUbiquityContainerIdentifier: para recuperar um URL para o diretório de contêiner do iCloud no qual você deseja armazenar o item. Use o URL do diretório do contêiner para construir um novo URL que especifica a localização do item no iCloud. Chame o método setUbiquitous: itemAtURL: destinationURL: error: de NSFileManager para mover o item para o iCloud. Nunca chame esse método do thread principal do seu aplicativo; fazer isso pode bloquear seu thread principal por um longo período de tempo ou causar um deadlock com um dos apresentadores de arquivo do próprio aplicativo. Quando você move um arquivo ou diretório para o iCloud, o sistema copia esse item da caixa de proteção do aplicativo para um diretório local privado para que possa ser monitorado pelo daemon do iCloud. Mesmo que o arquivo não esteja mais em sua sandbox, seu aplicativo ainda tem acesso total a ele. Embora uma cópia do arquivo permaneça local para o dispositivo atual, o arquivo também é enviado ao iCloud para que possa ser distribuído para outros dispositivos. O daemon iCloud lida com todo o trabalho de garantir que as cópias locais sejam as mesmas. Portanto, da perspectiva do seu aplicativo, o arquivo está apenas no iCloud.

Todas as alterações feitas em um arquivo ou diretório no iCloud devem ser feitas usando um objeto coordenador de arquivo. Essas alterações incluem mover, excluir, copiar ou renomear o item. O coordenador do arquivo garante que o daemon do iCloud não altere o arquivo ou diretório ao mesmo tempo e garante que outras partes interessadas sejam notificadas sobre as alterações feitas.

No entanto, se você se aprofundar um pouco mais nos documentos sobre setUbiquitous, encontrará:

Use este método para mover um arquivo de seu local atual para o iCloud. Para arquivos localizados na sandbox de um aplicativo, isso envolve a remoção física do arquivo do diretório da sandbox . (O sistema estende os privilégios da sandbox do seu aplicativo para dar a ele acesso aos arquivos que ele move para o iCloud.) Você também pode usar esse método para mover arquivos do iCloud e de volta para um diretório local.

Portanto, isso parece significar que um arquivo / diretório é excluído da caixa de proteção local e movido para a nuvem.

deixa pra lá
fonte
1
o link do url está quebrado ...
ngb
5

Tenho usado o seu exemplo e gosto dele por me ajudar a entender os fundamentos do iCloud. Agora estou discutindo com sua pergunta sobre meu próprio aplicativo, que deve oferecer suporte a usuários existentes do aplicativo com conteúdo armazenado localmente que podem ou não estar usando o iCloud, criando estes casos, pelo que posso dizer:

Casos:

  1. Novo usuário
    • tem icloud - criar documentos em icloud
    • no iCloud - criar documentos localmente
  2. Usuário existente
    • tem icloud
      • acabou de adicionar - migre documentos locais para o icloud
      • não apenas adicionado - abrir / salvar documentos no icloud
    • não iCloud
      • acabado de remover - migre os documentos antigos do icloud para o local
      • não apenas removido - abre / salva documentos no local

Se alguém remover o iCloud - as chamadas para um URL onipresente não retornariam nulo? Se for esse o caso, como faço para migrar os documentos de volta para o armazenamento local? Vou criar uma preferência do usuário por enquanto, mas parece um pouco uma solução alternativa.

Sinto que estou perdendo algo óbvio aqui, então se alguém puder ver, por favor, entre em contato.

ganhador
fonte
Devo acrescentar que estou me perguntando se há uma classe que lida com esses casos, então eu apenas a uso e não preciso me preocupar com onde salvá-la.
ganha em
Dê uma olhada em developer.apple.com/library/ios/#documentation/DataManagement/… que fornece alguns códigos de amostra para determinar se algo deve ser colocado na caixa de proteção local ou na nuvem.
n.evermind
Obrigado por isso. Eu tinha visto aquele documento, mas antes em minha missão no iCloud, então esqueci o código que ele oferece. Vou tentar adaptar sua amostra para oferecer suporte local e remoto. Ainda não estou certo de como lidamos com o usuário que desativa o iCloud, já que perdemos a URL onipresente, mas terei um crack e compartilharei uma atualização.
ganha em
1
Então, de certa forma, é um pouco estúpido termos que usar URLs para a nuvem e PATHs para a sandbox local. Seria bom se o iCloud pudesse cuidar de tudo para nós ... mas, dessa forma, basicamente precisamos codificar dois métodos diferentes para cada arquivo que abrimos.
n.evermind
Acabei de reler sua postagem. Agora estou salvando a preferência do usuário (ou seja, o usuário deseja / não deseja usar o iCloud) em NSUserDefaults. Isso é o que a Apple também sugere. Sempre verifico se o iCloud está acessível. Se não estiver acessível, digo aos usuários para ativá-lo - mas apenas se eles não tiverem dito explicitamente ao aplicativo que não desejam usá-lo. Caso contrário, torna-se irritante para quem não deseja usar o iCloud. Depois de determinar se o iCloud está ativado, irei seguir a rota ubíqua de URL e usar o UIDocument OU simplesmente abrirei os arquivos da sandbox como nos velhos tempos.
n.evermind
4

Se você deseja que os usuários possam compartilhar texto entre dispositivos anteriores ao iOS 5.0, você terá que fazer o que todos tinham que fazer antes do iCloud e mover as informações para seu próprio servidor.

Tudo o que você realmente precisa é de um servidor em algum lugar que permita que seu aplicativo salve seus arquivos de texto e os associe a uma conta de usuário.

Você precisará que os usuários criem uma conta e que você mesmo gerencie o processo de mover novas informações de um dispositivo para sua própria 'nuvem'.

Os usuários se registrarão com a mesma conta em outros dispositivos e você precisará tomar cuidado para detectar quando outro dispositivo moveu dados para sua própria nuvem e atualizar o dispositivo atual com as novas informações.

Obviamente, para dispositivos iOS 5.0, você provavelmente desejará detectar arquivos alterados para dispositivos pré-iOS 5.0 em sua própria nuvem e também poder falar com o iCloud.

Jonathan Watmough
fonte
Obrigado. Em outras palavras, se eu não quiser oferecer suporte a dispositivos pré-iOS 5, simplesmente vou com UIDocument e esqueço o conteúdo do diretório doc na caixa de proteção do meu aplicativo.
n.evermind
Basicamente, embora pelo que eu saiba, você ainda terá um documento na sandbox que o UIDocument ajudará a mediar com o iCloud para você, mas você será informado quando puder acessá-lo ... Ainda estou conseguindo para lidar com essas coisas sozinho!
Jonathan Watmough
3

Não parece que você está lutando tanto com o problema do iCloud / notICloud quanto com o do iOS5 / notIOS5.

Se o seu destino de implantação for iOS5, simplesmente sempre use a estrutura UIDocument. Se for onipresente, seu NSMetaDataQuery o encontrará na nuvem; caso contrário, ele o encontrará no dispositivo.

Se, por outro lado, você deseja fornecer acesso pré-5.0 ao seu aplicativo, então você precisará verificar condicionalmente se o iOS em execução é 5.0 ou superior. Se for, use UIDocument; se não, então leia / grave os dados da maneira antiga.

Minha abordagem foi escrever um método saveData condicional que verifica o iOS5. Se existir, eu atualizo a contagem de alterações (ou uso um gerenciador de desfazer). No seu caso, o textViewDidChange chamaria esse método. Caso contrário, ele salva no disco da maneira antiga. No carregamento, acontece o contrário.

Michael
fonte
1

Você fica confuso ao dizer "Trate os arquivos no iCloud da mesma forma que trata todos os outros arquivos na caixa de proteção do aplicativo". Isso vale para algo como o Keynote e Numbers, onde você mantém um monte de arquivos e, se tiver o iCloud, eles começam a sincronizar magicamente.

No entanto, você está construindo algo que depende de uma funcionalidade semelhante ao iCloud. Você não pode manter essa afirmação porque seu aplicativo depende do iCloud para estar presente para que tudo funcione da maneira que deve. Você terá que fechar seu aplicativo e simplesmente dizer "por favor, configure o iCloud para que isso funcione" ou duplique a funcionalidade semelhante ao iCloud (sua ou de outra pessoa) que você sempre pode usar, independentemente.

Jesper
fonte
Obrigado. Então acho que tenho que escolher se faço um aplicativo apenas para iCloud ou algum tipo de híbrido para pessoas que desativam a funcionalidade do iCloud. Como o iCloud é tão complexo, eu costumo escolher um aplicativo apenas para iCloud. Obrigado.
n.evermind