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.
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?
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 **)outError
para obter arquivos da nuvem e, em seguida, -(id)contentsForType:(NSString *)typeName error:(NSError **)outError
para 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:
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];
}
fonte
Respostas:
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:
No entanto, se você se aprofundar um pouco mais nos documentos sobre setUbiquitous, encontrará:
Portanto, isso parece significar que um arquivo / diretório é excluído da caixa de proteção local e movido para a nuvem.
fonte
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:
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.
fonte
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.
fonte
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.
fonte
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.
fonte