Principes de base d'iCloud et exemple de code [fermé]

85

En tant que débutant, j'ai du mal avec iCloud. Il existe quelques exemples, mais ils sont généralement assez détaillés (sur le forum des développeurs, il y en a un pour iCloud et CoreData qui est énorme). Les documents Apple sont OK, mais je ne peux toujours pas voir la situation dans son ensemble. Je vous en prie, soyez patient, certaines de ces questions sont assez fondamentales, mais peut-être faciles à répondre.

Contexte: J'ai une application iCloud très simple en cours d'exécution (exemple de code complet ci-dessous). Il n'y a qu'un seul UITextView montré à l'utilisateur et son entrée est enregistrée dans un fichier appelé text.txt.

entrez la description de l'image ici

Le fichier txt est poussé vers le cloud et mis à la disposition de tous les appareils. Fonctionne parfaitement, mais:

Problème principal: qu'en est-il des utilisateurs qui n'utilisent pas iCloud?

Lorsque je lance mon application (voir le code ci-dessous), je vérifie si l'utilisateur a activé iCloud. Si iCloud est activé, tout va bien. L'application continue et recherche text.txt dans le cloud. S'il est trouvé, il le chargera et l'affichera à l'utilisateur. Si text.txt n'est pas trouvé dans le cloud, il créera simplement un nouveau text.txt et l'affichera à l'utilisateur.

Si l'utilisateur n'a pas activé iCloud, rien ne se passera. Comment faire en sorte que les utilisateurs non-iCloud puissent toujours travailler avec mon application de texte? Ou est-ce que je les ignore simplement? Aurais-je besoin d'écrire des fonctions distinctes pour les utilisateurs non-iCloud? Ie fonctions dans lesquelles je charge simplement un text.txt à partir du dossier documents?

Apple écrit :

Traitez les fichiers dans iCloud de la même manière que vous traitez tous les autres fichiers dans le bac à sable de votre application.

Cependant, dans mon cas, il n'y a plus de sandbox d'application `` normale ''. C'est dans le cloud. Ou dois-je toujours charger d'abord mon text.txt à partir du disque, puis vérifier avec iCloud s'il y a quelque chose de plus à jour?

Problème connexe: structure de fichiers - Sandbox vs Cloud

Peut-être que mon problème principal est un malentendu fondamental sur la façon dont iCloud est censé fonctionner. Lorsque je crée une nouvelle instance d'un UIDocument, je vais devoir remplacer deux méthodes. D'abord - (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outErrorpour obtenir des fichiers du cloud, puis -(id)contentsForType:(NSString *)typeName error:(NSError **)outErrorpour les transférer dans le cloud.

Dois-je incorporer des fonctions séparées qui enregistreront également une copie locale de text.txt dans mon bac à sable? Cela fonctionnera-t-il pour les utilisateurs non-iCloud? Si je comprends bien iCloud, il enregistrera automatiquement une copie locale de text.txt. Il ne devrait donc pas être nécessaire que je sauvegarde quoi que ce soit dans le `` vieux '' bac à sable de mon application (c'est-à-dire comme c'était le cas dans l'ancien temps avant iCloud). À l'heure actuelle, mon bac à sable est totalement vide, mais je ne sais pas si c'est correct. Dois-je conserver une autre copie de text.txt là-dedans? Cela me donne l'impression d'encombrer ma structure de données ... car il y a un text.txt dans le cloud, un dans le bac à sable iCloud sur mon appareil (qui fonctionnera même si je suis hors ligne) et un troisième dans le bon vieux bac à sable de mon appli ...


MON CODE: Un exemple de code iCloud simple

Ceci est vaguement basé sur un exemple que j'ai trouvé dans le forum des développeurs et sur la vidéo de la session WWDC. Je l'ai dépouillé au strict minimum. Je ne suis pas sûr que ma structure MVC soit bonne. Le modèle est dans l'AppDelegate, ce qui n'est pas idéal. Toutes les suggestions pour l'améliorer sont les bienvenues.


EDIT: J'ai essayé d'extraire la question principale et l'ai postée [ici]. 4


APERÇU:

Aperçu

Le bit le plus important qui charge le text.txt depuis le cloud:

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

Le 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

LE 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];

}
ça ne fait rien
la source
4
Je suggérerais vraiment de diviser cela en quelques questions. Je vois quelques questions différentes enfouies ici, et il est difficile de les repérer dans le mur de texte que vous avez ici. Je reviendrais à cette question en demandant simplement ce qu'il faut faire pour les personnes qui n'ont pas activé iCloud, et diviserais les autres (avec uniquement les parties pertinentes de votre exemple de code) en questions séparées. Ce sont de bonnes questions, mais je pense qu'elles devraient être séparées.
Brad Larson
@BradLarson Merci pour votre commentaire. Je suis désolé si les questions sont un peu confuses, mais je pense que la question principale (comme j'ai essayé de l'indiquer) est le problème du bac à sable de l'application par rapport au bac à sable iCloud. J'ai fourni le code complet (qui est l'exemple de code iCloud le plus court, btw) car je pensais que le contexte ENTIER est vital pour savoir ce qui se passe ... Mais je pourrais simplement ouvrir une autre question et la relier à cette question à avoir une vue d'ensemble.
n.evermind
@BradLarson OK, j'ai ouvert une nouvelle question ici: stackoverflow.com/questions/7798555
...
Pour ceux qui essaient encore de se familiariser avec Core Data et iCloud, essayez ce lien ossh.com.au/design-and-technology/software-development
Duncan Groenewald
Ne devrait pas être fermé, c'est en fait l'un des messages les plus constructifs que j'ai vus sur iCloud ..
Andrew Smith

Réponses:

22

Je viens de relire la documentation et il semble que mon approche générale soit erronée. Je dois d'abord créer le fichier dans le bac à sable, puis le déplacer vers le cloud. En d'autres termes, Apple semble suggérer que je devrais avoir trois versions du même fichier à tout moment: une dans le répertoire de mon application, une dans le répertoire démon iCloud de mon appareil (qui est également accessible en mode hors connexion) et une dans le nuage:

Les applications utilisent les mêmes technologies pour gérer les fichiers et les répertoires dans iCloud que pour les fichiers et répertoires locaux. Les fichiers et répertoires dans iCloud ne sont toujours que des fichiers et des répertoires. Vous pouvez les ouvrir, les créer, les déplacer, les copier, les lire et les écrire, les supprimer ou toute autre opération que vous pourriez souhaiter effectuer. La seule différence entre les fichiers et répertoires locaux et les fichiers et répertoires iCloud est l'URL que vous utilisez pour y accéder. Au lieu que les URL soient relatives au bac à sable de votre application, les URL des fichiers et répertoires iCloud sont relatives au répertoire de conteneur iCloud correspondant.

Pour déplacer un fichier ou un répertoire vers iCloud:

Créez le fichier ou le répertoire localement dans le sandbox de votre application. Lors de son utilisation, le fichier ou le répertoire doit être géré par un présentateur de fichier, tel qu'un objet UIDocument.

Utilisez la méthode URLForUbiquityContainerIdentifier: pour récupérer une URL pour le répertoire de conteneur iCloud dans lequel vous souhaitez stocker l'élément. Utilisez l'URL du répertoire du conteneur pour créer une nouvelle URL qui spécifie l'emplacement de l'élément dans iCloud. Appelez la méthode setUbiquitous: itemAtURL: destinationURL: error: de NSFileManager pour déplacer l'élément vers iCloud. N'appelez jamais cette méthode à partir du fil de discussion principal de votre application; cela pourrait bloquer votre thread principal pendant une période prolongée ou provoquer un blocage avec l'un des propres présentateurs de fichiers de votre application. Lorsque vous déplacez un fichier ou un répertoire vers iCloud, le système copie cet élément hors de votre sandbox d'application et dans un répertoire local privé afin qu'il puisse être surveillé par le démon iCloud. Même si le fichier ne se trouve plus dans votre bac à sable, votre application y a toujours un accès complet. Bien qu'une copie du fichier reste locale sur l'appareil actuel, le fichier est également envoyé à iCloud afin qu'il puisse être distribué à d'autres appareils. Le démon iCloud gère tout le travail de s'assurer que les copies locales sont les mêmes. Donc, du point de vue de votre application, le fichier est simplement dans iCloud.

Toutes les modifications que vous apportez à un fichier ou à un répertoire dans iCloud doivent être effectuées à l'aide d'un objet de coordinateur de fichiers. Ces modifications incluent le déplacement, la suppression, la copie ou le changement de nom de l'élément. Le coordinateur de fichiers s'assure que le démon iCloud ne modifie pas le fichier ou le répertoire en même temps et veille à ce que les autres parties intéressées soient informées des modifications que vous apportez.

Cependant, si vous approfondissez un peu plus la documentation concernant setUbiquitous, vous trouverez:

Utilisez cette méthode pour déplacer un fichier de son emplacement actuel vers iCloud. Pour les fichiers situés dans le bac à sable d'une application, cela implique de supprimer physiquement le fichier du répertoire bac à sable . (Le système étend les privilèges sandbox de votre application pour lui donner accès aux fichiers qu'elle déplace vers iCloud.) Vous pouvez également utiliser cette méthode pour déplacer des fichiers hors d'iCloud et les renvoyer dans un répertoire local.

Cela semble donc signifier qu'un fichier / répertoire est supprimé du bac à sable local et déplacé dans le cloud.

ça ne fait rien
la source
1
Le lien url est rompu ...
ngb
5

J'ai utilisé votre exemple et je l'aime pour m'aider à comprendre les bases d'iCloud. Maintenant, je suis en train de me débattre avec votre question pour ma propre application qui doit prendre en charge les utilisateurs existants de l'application avec du contenu stocké localement qui peuvent ou non utiliser iCloud en créant ces cas pour autant que je sache:

Cas:

  1. Nouvel utilisateur
    • has icloud - créer des documents dans icloud
    • pas d'icloud - créer des documents localement
  2. Utilisateur existant
    • a icloud
      • vient d'être ajouté - migrer les documents locaux vers icloud
      • pas seulement ajouté - ouvrir / enregistrer des documents sur icloud
    • pas d'icloud
      • vient d'être supprimé - migrer les anciens documents icloud vers les fichiers locaux
      • pas seulement supprimé - ouvrir / enregistrer des documents en local

Si quelqu'un supprime iCloud, les appels à une URL omniprésente ne renverront-ils pas? Si tel est le cas, comment puis-je migrer les documents vers le stockage local? Je vais créer un utilisateur préféré pour le moment, mais cela semble un peu une solution de contournement.

J'ai l'impression qu'il me manque quelque chose d'évident ici, alors si quelqu'un peut le voir, veuillez intervenir.

gagner
la source
Je devrais ajouter que je me demande s'il existe une classe qui gère ces cas, alors je l'utilise simplement et je n'ai pas à me soucier de savoir où l'enregistrer.
Earnshavian
Jetez un œil à developer.apple.com/library/ios/#documentation/DataManagement/… qui donne un exemple de code pour déterminer si quelque chose doit être placé dans le bac à sable local ou dans le cloud.
n.evermind
Merci pour ça. J'avais vu ce document mais plus tôt dans ma mission iCloud, j'avais donc oublié le code qu'il propose. Je vais essayer d'adapter votre échantillon pour prendre en charge local et distant. Je ne sais toujours pas comment nous gérons l'utilisateur qui désactive iCloud puisque nous perdons l'URL omniprésente, mais je vais avoir une fissure et partager une mise à jour.
Earnshavian
1
Donc, d'une certaine manière, c'est un peu stupide que nous devions utiliser des URL pour le cloud et des PATH pour le sandbox local. Ce serait bien si iCloud pouvait tout gérer pour nous ... mais de cette façon, nous devons fondamentalement coder deux méthodes différentes pour chaque fichier que nous ouvrons.
n.evermind
Je viens de relire votre message. J'enregistre maintenant les préférences de l'utilisateur (c'est-à-dire que l'utilisateur veut / ne veut pas utiliser iCloud) dans NSUserDefaults. C'est aussi ce que suggère Apple. Je vérifie toujours si iCloud est accessible. S'il n'est pas accessible, je dis aux utilisateurs de l'activer - mais seulement s'ils n'ont pas explicitement indiqué à l'application qu'ils ne souhaitaient pas l'utiliser. Sinon, cela devient ennuyeux pour ceux qui ne souhaitent pas utiliser iCloud. Une fois que j'ai déterminé si iCloud est activé, je suivrai la route URL omniprésente et utiliserai UIDocument OU ouvrirai simplement les fichiers du bac à sable comme au bon vieux temps.
n.evermind
4

Si vous voulez que les utilisateurs puissent partager du texte entre des appareils antérieurs à iOS 5.0, vous devrez faire ce que tout le monde devait faire avant iCloud et déplacer les informations vers votre propre serveur.

Tout ce dont vous avez vraiment besoin est un serveur quelque part qui permet à votre application d'enregistrer ses fichiers texte et de les associer à un compte utilisateur.

Vous aurez besoin des utilisateurs pour créer un compte et vous devrez gérer vous-même le processus de transfert de nouvelles informations sur un appareil dans votre propre «cloud».

Les utilisateurs s'enregistreront avec le même compte sur d'autres appareils et vous devrez vous assurer de détecter lorsqu'un autre appareil a déplacé des données sur votre propre cloud et mettre à jour l'appareil actuel avec les nouvelles informations.

Évidemment, pour les appareils iOS 5.0, vous voudrez probablement détecter les fichiers modifiés pour les appareils pré-iOS 5.0 dans votre propre cloud, et également pouvoir parler à iCloud.

Jonathan Watmough
la source
Merci. Donc, en d'autres termes, si je ne veux pas prendre en charge les appareils pré-iOS 5, j'utilise simplement UIDocument et j'oublie le contenu du répertoire doc dans le bac à sable de mon application.
n.evermind
À peu près, mais pour autant que je sache, vous aurez toujours un document dans le bac à sable dont UIDocument vous aidera à servir de médiateur avec iCloud, mais on vous dira quand vous pourrez y accéder ... Je reçois toujours faire face à ce truc moi-même!
Jonathan Watmough
3

Il ne semble pas que vous ayez autant de difficultés avec un problème iCloud / notICloud qu'un problème iOS5 / notIOS5.

Si votre cible de déploiement est iOS5, utilisez simplement toujours la structure UIDocument. S'il est omniprésent, votre NSMetaDataQuery le trouvera dans le cloud; sinon, il le trouvera sur l'appareil.

Si, en revanche, vous souhaitez fournir un accès pré-5.0 à votre application, vous devrez vérifier conditionnellement si l'iOS en cours d'exécution est 5.0 ou supérieur. Si c'est le cas, utilisez UIDocument; sinon, lisez / écrivez les données à l'ancienne.

Mon approche a été d'écrire une méthode saveData conditionnelle qui vérifie iOS5. S'il existe, je mets à jour le nombre de modifications (ou j'utilise un gestionnaire d'annulation). Dans votre cas, textViewDidChange appellerait cette méthode. Sinon, il enregistre sur le disque à l'ancienne. Au chargement, c'est l'inverse qui se produit.

Michael
la source
1

Vous êtes déconcerté par "Traitez les fichiers dans iCloud de la même manière que vous traitez tous les autres fichiers dans le sandbox de votre application." Cela est vrai pour quelque chose comme Keynote et Numbers où vous conservez un tas de fichiers, et si vous avez iCloud, ils commencent à se synchroniser comme par magie.

Cependant, vous créez quelque chose qui dépend des fonctionnalités de type iCloud. Vous ne pouvez pas vous en tenir à cette déclaration, car votre application dépend d'iCloud pour être présente pour que tout fonctionne comme prévu. Vous devrez soit fermer votre application et dire simplement «veuillez configurer iCloud pour que cela fonctionne» ou dupliquer une fonctionnalité de type iCloud (la vôtre ou celle de quelqu'un d'autre) que vous pouvez toujours utiliser, peu importe.

Jesper
la source
Merci. Je suppose donc que je dois choisir si je fais une application iCloud uniquement ou une sorte d'hybride pour les personnes qui désactivent la fonctionnalité iCloud. Comme iCloud est si complexe, j'ai tendance à opter pour une application iCloud uniquement. Merci.
n.evermind