Podstawy iCloud i przykład kodu [zamknięte]

85

Jako początkujący zmagam się z iCloud. Jest kilka próbek, ale zwykle są one dość szczegółowe (na forum programistów jest jeden dla iCloud i CoreData, który jest ogromny). Dokumenty Apple są w porządku, ale nadal nie widzę pełnego obrazu. Więc proszę o wyrozumiałość, niektóre z tych pytań są dość fundamentalne, ale prawdopodobnie łatwe do udzielenia odpowiedzi.

Kontekst: Mam uruchomioną bardzo prostą aplikację iCloud (pełny przykładowy kod poniżej). Jest tylko jeden UITextView wyświetlany użytkownikowi, a jego / jej dane wejściowe są zapisywane w pliku o nazwie text.txt.

wprowadź opis obrazu tutaj

Plik txt jest przesyłany do chmury i udostępniany wszystkim urządzeniom. Działa idealnie, ale:

Główny problem: A co z użytkownikami, którzy nie używają iCloud?

Kiedy uruchamiam moją aplikację (patrz kod poniżej), sprawdzam, czy użytkownik ma włączoną usługę iCloud. Jeśli iCloud jest włączony, wszystko jest w porządku. Aplikacja działa dalej i szuka text.txt w chmurze. Jeśli zostanie znaleziony, załaduje go i wyświetli użytkownikowi. Jeśli text.txt nie zostanie znaleziony w chmurze, po prostu utworzy nowy text.txt i wyświetli go użytkownikowi.

Jeśli użytkownik nie ma włączonej usługi iCloud, nic się nie stanie. Jak sprawię, że użytkownicy spoza iCloud będą mogli nadal pracować z moją aplikacją tekstową? A może po prostu je ignoruję? Czy musiałbym pisać osobne funkcje dla użytkowników spoza iCloud? Czyli funkcje, w których po prostu ładuję plik text.txt z folderu dokumentów?

Apple pisze :

Traktuj pliki w iCloud tak samo, jak wszystkie inne pliki w piaskownicy aplikacji.

Jednak w moim przypadku nie ma już „normalnej” piaskownicy aplikacji. Jest w chmurze. A może zawsze najpierw ładuję plik text.txt z dysku, a następnie sprawdzam w iCloud, czy jest coś bardziej aktualnego?

Powiązany problem: Struktura plików - piaskownica a chmura

Być może moim głównym problemem jest fundamentalne niezrozumienie tego, jak powinien działać iCloud. Kiedy tworzę nowe wystąpienie UIDocument, będę musiał nadpisać dwie metody. Najpierw - (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outErrorpobrać pliki z chmury, a następnie -(id)contentsForType:(NSString *)typeName error:(NSError **)outErrorprzenieść je do chmury.

Czy muszę włączać oddzielne funkcje, które będą również zapisywać lokalną kopię pliku text.txt w mojej piaskownicy? Czy to zadziała dla użytkowników spoza iCloud? Jak rozumiem, iCloud automatycznie zapisze lokalną kopię pliku text.txt. Więc nie powinno być potrzeby, abym zapisywał cokolwiek w „starej” piaskownicy mojej aplikacji (czyli tak jak to było w dawnych czasach sprzed iCloud). W tej chwili moja piaskownica jest całkowicie pusta, ale nie wiem, czy to prawda. Czy powinienem przechowywać tam inną kopię pliku text.txt? Wydaje mi się, że zaśmiecają moją strukturę danych ... ponieważ jeden plik text.txt znajduje się w chmurze, jeden w piaskownicy iCloud na moim urządzeniu (który będzie działać, nawet jeśli jestem offline), a trzeci w starej, dobrej piaskownicy moja aplikacja ...


MÓJ KOD: Prosty przykładowy kod iCloud

Jest to luźno oparte na przykładzie, który znalazłem na forum programistów i na wideo z sesji WWDC. Ogarnąłem go do absolutnego minimum. Nie jestem pewien, czy moja struktura MVC jest dobra. Model znajduje się w AppDelegate, co nie jest idealne. Wszelkie sugestie, jak to poprawić, są mile widziane.


EDYCJA: Próbowałem wyodrębnić główne pytanie i zamieściłem je [tutaj]. 4


PRZEGLĄD:

Przegląd

Najważniejszy bit, który ładuje plik text.txt z chmury:

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

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

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

}
nieważne
źródło
4
Naprawdę sugerowałbym podzielenie tego na kilka pytań. Widzę tutaj kilka różnych pytań i trudno jest je znaleźć na ścianie tekstu, którą tu masz. Przywróciłbym to pytanie z powrotem do pytania o to, co zrobić dla osób, które nie mają włączonej usługi iCloud, i podzielę pozostałe (tylko z odpowiednimi częściami przykładowego kodu) na osobne pytania. To dobre pytania, ale myślę, że należy je rozdzielić.
Brad Larson
@BradLarson Dzięki za komentarz. Przepraszam, jeśli pytania są nieco pomieszane, ale myślę, że głównym pytaniem (jak próbowałem wskazać) jest problem piaskownicy aplikacji i piaskownicy iCloud. Podałem pełny kod (który jest najkrótszym przykładem kodu iCloud, przy okazji), ponieważ uważałem, że CAŁY kontekst jest niezbędny, aby wiedzieć, co się dzieje ... Ale mogę po prostu otworzyć kolejne pytanie i połączyć je z powrotem z tym pytaniem uzyskać szerszy obraz.
n.evermind
@BradLarson OK, otworzyłem tutaj nowe pytanie: stackoverflow.com/questions/7798555/…
n.evermind
Dla tych, którzy wciąż próbują zrozumieć Core Data i iCloud, wypróbuj ten link ossh.com.au/design-and-technology/software-development/…
Duncan Groenewald,
Nie powinno być zamykane, jest to właściwie jeden z bardziej konstruktywnych postów, które widziałem w iCloud ..
Andrew Smith

Odpowiedzi:

22

Właśnie ponownie przeczytałem dokumentację i wydaje się, że moje ogólne podejście jest złe. Najpierw powinienem utworzyć plik w piaskownicy, a następnie przenieść go do chmury. Innymi słowy, Apple wydaje się sugerować, że powinienem zawsze mieć trzy wersje tego samego pliku: jedną w katalogu mojej aplikacji, jedną w katalogu demona iCloud na moim urządzeniu (który jest również dostępny w trybie offline) i jedną w Chmura:

Aplikacje używają tych samych technologii do zarządzania plikami i katalogami w iCloud, jak w przypadku lokalnych plików i katalogów. Pliki i katalogi w iCloud to nadal tylko pliki i katalogi. Możesz je otwierać, tworzyć, przenosić, kopiować, czytać i pisać z nich, usuwać lub wykonywać dowolne inne operacje, które chcesz wykonać. Jedyną różnicą między lokalnymi plikami i katalogami a plikami i katalogami iCloud jest adres URL, którego używasz, aby uzyskać do nich dostęp. Zamiast adresów URL odnoszących się do piaskownicy aplikacji, adresy URL plików i katalogów iCloud są względne względem odpowiedniego katalogu kontenera iCloud.

Aby przenieść plik lub katalog do iCloud:

Utwórz plik lub katalog lokalnie w piaskownicy aplikacji. W trakcie używania plik lub katalog musi być zarządzany przez prezentera plików, na przykład obiekt UIDocument.

Użyj metody URLForUbiquityContainerIdentifier:, aby pobrać adres URL katalogu kontenera iCloud, w którym chcesz przechowywać element. Użyj adresu URL katalogu kontenera, aby utworzyć nowy adres URL, który określa lokalizację elementu w iCloud. Wywołaj metodę setUbiquitous: itemAtURL: destinationURL: error: metoda NSFileManager, aby przenieść element do iCloud. Nigdy nie wywołuj tej metody z głównego wątku aplikacji; może to spowodować zablokowanie głównego wątku na dłuższy czas lub zablokowanie jednego z własnych programów do prezentacji plików w aplikacji. Kiedy przenosisz plik lub katalog do iCloud, system kopiuje ten element z piaskownicy aplikacji do prywatnego katalogu lokalnego, aby mógł być monitorowany przez demona iCloud. Mimo że plik nie znajduje się już w piaskownicy, aplikacja nadal ma do niego pełny dostęp. Chociaż kopia pliku pozostaje lokalna dla bieżącego urządzenia, plik jest również wysyłany do iCloud, aby można go było rozpowszechniać na innych urządzeniach. Demon iCloud obsługuje całą pracę polegającą na upewnianiu się, że lokalne kopie są takie same. Z punktu widzenia Twojej aplikacji plik jest po prostu w iCloud.

Wszystkie zmiany wprowadzone w pliku lub katalogu w iCloud muszą zostać wprowadzone przy użyciu obiektu koordynatora plików. Te zmiany obejmują przenoszenie, usuwanie, kopiowanie lub zmianę nazwy elementu. Koordynator plików zapewnia, że ​​demon iCloud nie zmienia pliku lub katalogu w tym samym czasie i zapewnia, że ​​inne zainteresowane strony są powiadamiane o wprowadzanych zmianach.

Jeśli jednak zagłębisz się w dokumentację dotyczącą setUbiquitous, znajdziesz:

Użyj tej metody, aby przenieść plik z jego bieżącej lokalizacji do iCloud. W przypadku plików znajdujących się w piaskownicy aplikacji obejmuje to fizyczne usunięcie pliku z katalogu piaskownicy . (System rozszerza uprawnienia piaskownicy aplikacji, aby umożliwić jej dostęp do plików, które przenosi do iCloud). Możesz także użyć tej metody, aby przenieść pliki z iCloud z powrotem do katalogu lokalnego.

Wydaje się więc, że oznacza to, że plik / katalog zostanie usunięty z lokalnej piaskownicy i przeniesiony do chmury.

nieważne
źródło
1
link do
adresu
5

Korzystam z twojego przykładu i podoba mi się, że pomógł mi zrozumieć podstawy iCloud. Teraz kłócę się z Twoim pytaniem dotyczącym mojej własnej aplikacji, która musi obsługiwać istniejących użytkowników aplikacji lokalnie przechowywanymi treściami, którzy mogą, ale nie muszą, używać iCloud do tworzenia takich przypadków, o ile wiem:

Przypadki:

  1. Nowy użytkownik
    • ma icloud - twórz dokumenty w icloud
    • no icloud - twórz dokumenty lokalnie
  2. Istniejący użytkownik
    • ma icloud
      • właśnie dodane - migracja lokalnych dokumentów do icloud
      • nie tylko dodane - otwórz / zapisz dokumenty w icloud
    • brak icloud
      • właśnie usunięto - przenieś poprzednie dokumenty icloud do lokalnego
      • nie tylko usunięte - otwórz / zapisz dokumenty do lokalnego

Jeśli ktoś usunie iCloud - czy wywołania wszechobecnego adresu URL nie zwrócą zera? Jeśli tak jest, jak mogę przenieść dokumenty z powrotem do magazynu lokalnego? Na razie utworzę preferencję użytkownika, ale wydaje się, że jest to trochę obejście.

Czuję, że brakuje mi czegoś oczywistego, więc jeśli ktoś może to zobaczyć, zadzwoń.

zarabiać
źródło
Powinienem dodać, że zastanawiam się, czy istnieje klasa, która obsługuje te przypadki, więc po prostu jej używam i nie muszę się martwić, gdzie ją zapisać.
zarabiaj
Zajrzyj na stronę developer.apple.com/library/ios/#documentation/DataManagement/…, która zawiera przykładowy kod do określenia, czy coś należy umieścić w lokalnej piaskownicy, czy w chmurze.
n.evermind
Dziękuję za to. Widziałem ten dokument, ale wcześniej podczas mojej misji iCloud, więc zapomniałem kodu, który oferuje. Spróbuję dostosować twoją próbkę do obsługi lokalnej i zdalnej. Nadal nie jestem pewien, jak postępujemy z użytkownikiem, który wyłącza iCloud, ponieważ tracimy wszechobecny adres URL, ale będę miał pęknięcie i udostępnię aktualizację.
zarabiaj
1
Więc w pewnym sensie to trochę głupie, że musimy używać adresów URL dla chmury i PATH dla lokalnej piaskownicy. Byłoby miło, gdyby iCloud poradził sobie ze wszystkim za nas ... ale w ten sposób zasadniczo musimy zakodować dwie różne metody dla każdego otwieranego pliku.
n.evermind
Właśnie ponownie przeczytałem Twój post. Zapisuję teraz preferencje użytkownika (tj. Użytkownik chce / nie chce używać iCloud) w NSUserDefaults. To też sugeruje Apple. Zawsze sprawdzam, czy iCloud jest dostępny. Jeśli nie jest dostępna, mówię użytkownikom, aby ją włączyli - ale tylko wtedy, gdy nie powiedzieli wyraźnie aplikacji, że nie chcą jej używać. W przeciwnym razie będzie to denerwujące dla tych, którzy nie chcą korzystać z iCloud. Po ustaleniu, czy iCloud jest włączony, albo podążę za wszechobecną trasą URL i użyję UIDocument LUB po prostu otworzę pliki z piaskownicy, jak za starych dobrych czasów.
n.evermind
4

Jeśli chcesz, aby użytkownicy mogli udostępniać tekst między urządzeniami, które są starsze niż iOS 5.0, będziesz musiał zrobić to, co wszyscy musieli zrobić przed iCloud i przenieść informacje na własny serwer.

Wszystko, czego naprawdę potrzebujesz, to serwer gdzieś, który pozwala aplikacji zapisywać pliki tekstowe i kojarzyć je z kontem użytkownika.

Będziesz potrzebował użytkowników, aby utworzyli konto i będziesz musiał samodzielnie zarządzać procesem przenoszenia nowych informacji z jednego urządzenia do własnej „chmury”.

Użytkownicy będą rejestrować się przy użyciu tego samego konta na innych urządzeniach i będziesz musiał zająć się wykrywaniem, kiedy inne urządzenie przeniosło dane do Twojej własnej chmury i zaktualizować bieżące urządzenie nowymi informacjami.

Oczywiście w przypadku urządzeń z systemem iOS 5.0 prawdopodobnie będziesz chciał wykryć zmienione pliki dla urządzeń wcześniejszych niż iOS 5.0 we własnej chmurze, a także móc rozmawiać z iCloud.

Jonathan Watmough
źródło
Dzięki. Innymi słowy, jeśli nie chcę obsługiwać urządzeń starszych niż iOS 5, po prostu idę z UIDocument i zapominam o zawartości katalogu doc ​​w piaskownicy mojej aplikacji.
n.evermind
Właściwie, o ile wiem, nadal będziesz mieć dokument w piaskownicy, który UIDocument pomoże ci w mediacji z iCloud, ale zostaniesz poinformowany, kiedy będziesz mieć do niego dostęp ... Nadal otrzymuję samemu sobie z tym poradzić!
Jonathan Watmough
3

Nie wygląda na to, że borykasz się z problemem iCloud / notICloud tak bardzo, jak problemem iOS5 / notIOS5.

Jeśli celem wdrożenia jest iOS5, po prostu zawsze używaj struktury UIDocument. Jeśli jest wszechobecny, wówczas NSMetaDataQuery znajdzie go w chmurze; jeśli nie, znajdzie go na urządzeniu.

Jeśli z drugiej strony chcesz zapewnić dostęp do swojej aplikacji w wersji wcześniejszej niż 5.0, musisz warunkowo sprawdzić, czy działający system iOS ma wersję 5.0 lub nowszą. Jeśli tak, użyj UIDocument; jeśli nie, to czytaj / zapisuj dane w stary sposób.

Moje podejście polegało na napisaniu warunkowej metody saveData, która sprawdza pod kątem iOS5. Jeśli istnieje, aktualizuję liczbę zmian (lub używam menedżera cofania). W twoim przypadku textViewDidChange wywoła tę metodę. Jeśli nie, to zapisuje na dysku w stary sposób. Podczas ładowania dzieje się odwrotnie.

Michał
źródło
1

Zaskoczyło Cię „Traktuj pliki w iCloud w taki sam sposób, jak wszystkie inne pliki w piaskownicy aplikacji”. Dotyczy to czegoś takiego jak Keynote i Numbers, w których przechowujesz kilka plików, a jeśli masz iCloud, zaczynają magicznie synchronizować.

Jednak tworzysz coś, co zależy od funkcjonalności podobnej do iCloud. Nie możesz trzymać się tego oświadczenia, ponieważ Twoja aplikacja zależy od iCloud, aby wszystko działało tak, jak powinno. Będziesz musiał zamknąć aplikację i po prostu powiedzieć „skonfiguruj iCloud, aby to działało” lub zduplikować funkcjonalność podobną do iCloud (własną lub kogoś innego), z której zawsze możesz korzystać, niezależnie od tego.

Jesper
źródło
Dzięki. Myślę więc, że muszę wybrać, czy robię aplikację tylko iCloud, czy jakąś hybrydę dla osób, które wyłączają funkcjonalność iCloud. Ponieważ iCloud jest tak skomplikowany, zwykle wybieram aplikację tylko iCloud. Dzięki.
n.evermind