iCloud basics and code sample

52,910

Solution 1

I just re-read the docs and it appears that my general approach is wrong. I should first create the file in the sandbox and then move it to the cloud. In other words, Apple seems to suggest that I should have three versions of the same file at all times: one in the directory of my app, one in the iCloud demon directory of my device (which is also accessible if offline) and one in the cloud:

Apps use the same technologies to manage files and directories in iCloud that they do for local files and directories. Files and directories in iCloud are still just files and directories. You can open them, create them, move them, copy them, read and write from them, delete them, or any of the other operations you might want to do. The only differences between local files and directories and iCloud files and directories is the URL you use to access them. Instead of URLs being relative to your app’s sandbox, URLs for iCloud files and directories are relative to the corresponding iCloud container directory.

To move a file or directory to iCloud:

Create the file or directory locally in your app sandbox. While in use, the file or directory must be managed by a file presenter, such as a UIDocument object.

Use the URLForUbiquityContainerIdentifier: method to retrieve a URL for the iCloud container directory in which you want to store the item. Use the container directory URL to build a new URL that specifies the item’s location in iCloud. Call the setUbiquitous:itemAtURL:destinationURL:error: method of NSFileManager to move the item to iCloud. Never call this method from your app’s main thread; doing so could block your main thread for an extended period of time or cause a deadlock with one of your app’s own file presenters. When you move a file or directory to iCloud, the system copies that item out of your app sandbox and into a private local directory so that it can be monitored by the iCloud daemon. Even though the file is no longer in your sandbox, your app still has full access to it. Although a copy of the file remains local to the current device, the file is also sent to iCloud so that it can be distributed to other devices. The iCloud daemon handles all of the work of making sure that the local copies are the same. So from the perspective of your app, the file just is in iCloud.

All changes you make to a file or directory in iCloud must be made using a file coordinator object. These changes include moving, deleting, copying, or renaming the item. The file coordinator ensures that the iCloud daemon does not change the file or directory at the same time and ensures that other interested parties are notified of the changes you make.

However, if you dig a little deeper into the docs concerning setUbiquitous, you'll find:

Use this method to move a file from its current location to iCloud. For files located in an application’s sandbox, this involves physically removing the file from the sandbox directory. (The system extends your application’s sandbox privileges to give it access to files it moves to iCloud.) You can also use this method to move files out of iCloud and back into a local directory.

So this appears to mean that a file / directory gets deleted form the local sandbox and moved into the cloud.

Solution 2

I've been using your example and I like it for helping me grasp the basics of iCloud. Now I'm wrangling with your question for my own app which has to support existing users of the app with locally stored content who may or may not be using iCloud creating these cases as far as I can tell:

Cases:

  1. New user
    • has icloud - create documents in icloud
    • no icloud - create documents locally
  2. Existing user
    • has icloud
      • just added - migrate local docs to icloud
      • not just added - open/save docs to icloud
    • no icloud
      • just removed - migrate former icloud docs to local
      • not just removed - open/save docs to local

If someone removes iCloud - wouldn't the calls to ubiquitous URL return nil? If that's the case how do I migrate the docs back to local storage? I'll create a user pref for now but seems a bit of a workaround.

I feel like I'm missing something obvious here so if anyone can see it, please chime in.

Solution 3

If you want users to be able to share text between devices that are pre-iOS 5.0, you are going to have to do what everyone had to do before iCloud and move information to your own server.

All you really need is a server somewhere that lets your app save its text files and associate them with a user account.

You'll need users to create an account and you'll need to manage the process yourself, of moving new information on one device into your own 'cloud'.

Users will register with the same account on other devices and you'll need to take care of detecting when another device has moved data onto your own cloud, and update the current device with the new info.

Obviously, for iOS 5.0 devices, you'll probably want to detect changed files for pre-iOS 5.0 devices in your own cloud, and also be able to talk to iCloud.

Solution 4

It doesn't seem that you are struggling with a iCloud/notICloud issue as much as a iOS5/notIOS5 issue.

If your deployment target is iOS5, then simply always use the UIDocument structure. If it is ubiquitous, then your NSMetaDataQuery will find it in the cloud; if not it will find it on the device.

If, on the other hand, you want to provide pre 5.0 access to your app, then you will need to conditionally check to see if if the running iOS is 5.0 or greater. If it is then use UIDocument; if not then read/write data the old way.

My approach was to write a conditional saveData method that checks for iOS5. If it exists I update the change count (or use an undo manager). In your case the textViewDidChange would call this method. If not, then it saves to disk the old way. On loading, the opposite happens.

Solution 5

You are befuddled by "Treat files in iCloud the same way you treat all other files in your app sandbox." This holds true for something like Keynote and Numbers where you keep a bunch of files, and if you have iCloud, they start syncing magically.

However, you're building something that depends on iCloud-like functionality. You can't hold onto that statement because your app depends on iCloud to be present for anything to work the way it's meant to. You will either have to close your app down and simply say "please setup iCloud for this to work" or duplicate iCloud-like functionality (your own or someone else's) that you can always use, regardless.

Share:
52,910
n.evermind
Author by

n.evermind

I speak: Copy-and-paste, Basic, Objective-C, Python, Django and have recently learned how to use the very retro but stylish terminal. Otherwise: Creator of digital-analog-but-stunningly beautiful notebooks in love with beautiful UIs

Updated on July 08, 2022

Comments

  • n.evermind
    n.evermind almost 2 years

    As a beginner, I'm struggling with iCloud. There are some samples, but they are usually quite detailed (on the developer forum there is one for iCloud and CoreData which is massive). The apple docs are OK, but I still can't see the big picture. So please bear with me, some of these questions are quite fundamental, but possibly easy to answer.

    Context: I have a very simple iCloud app running (full sample code below). There is only one UITextView shown to the user and his/her input is saved in a file called text.txt.

    enter image description here

    The txt file is pushed to the cloud and made available to all devices. Works perfectly, but:

    Main problem: What about users who do not use iCloud?

    When I launch my app (see code below), I check if the user has iCloud enabled. If iCloud is enabled, everything is fine. The app goes ahead and looks for text.txt in the cloud. If found, it will load it and display it to the user. If text.txt is not found in the cloud, it will simply create a new text.txt and will display that to the user.

    If the user does not have iCloud enabled, nothing will happen. How will I make it possible that non-iCloud users can still work with my text app? Or do I simply ignore them? Would I need to write separate functions for non-iCloud users? I.e. functions in which I simply load a text.txt from the documents folder?

    Apple writes:

    Treat files in iCloud the same way you treat all other files in your app sandbox.

    However, in my case there is no 'normal' app sandbox anymore. It's in the cloud. Or do I always load my text.txt from disk first and then check with iCloud if there is something more up-to-date?

    Related problem: File structure - Sandbox vs. Cloud

    Perhaps my main problem is a fundamental misunderstanding of how iCloud is supposed to work. When I create a new instance of an UIDocument, I'll have to overwrite two methods. First - (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError **)outError to get files from the cloud and then -(id)contentsForType:(NSString *)typeName error:(NSError **)outError to get files into the cloud.

    Do I have to incorporate separate functions which will also save a local copy of text.txt into my sandbox? Will this work for non-iCloud users? As I understand iCloud, it will save a local copy of text.txt automatically. So there shouldn't be any need for me save anything into the 'old' sandbox of my app (i.e. as it used to be in the old, pre-iCloud days). Right now, my sandbox is totally empty, but I don't know if this is correct. Should I keep another copy of text.txt in there? This feels like cluttering my data structure... as there is one text.txt in the cloud, one in the iCloud sandbox on my device (which will work even if I am offline), and a third one in the good old sandbox of my app...


    MY CODE: A simple iCloud sample code

    This is loosely based on an example I found in the developer forum and on the WWDC session video. I stripped it down to the bare minimum. I'm not sure that my MVC structure is any good. The model is in the AppDelegate which isn't ideal. Any suggestions to make it better are welcome.


    EDIT: I tried to extract the main question and posted it [here].4


    OVERVIEW:

    Overview

    The most important bit which loads the text.txt from the 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
    

    The 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
    

    THE 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];
    
    }
    
  • n.evermind
    n.evermind over 12 years
    Thanks. So in other words, if I don't want to support pre-iOS 5 devices, I simply go with UIDocument and forget about the contents of the doc directory in the sandbox of my app.
  • Jonathan Watmough
    Jonathan Watmough over 12 years
    Pretty much, though as far as I can tell, you'll still have a document in the sandbox which UIDocument will help mediate with iCloud for you, but you'll be told when you can access it... I'm still getting to grips with this stuff myself!
  • n.evermind
    n.evermind over 12 years
    Thanks. So I guess I have to choose whether I do an iCloud only app or some kind of hybrid for people who turn of iCloud functionality. As iCloud is so complex, I tend to go for an iCloud only app then. Thanks.
  • earnshavian
    earnshavian over 12 years
    I should add that I'm wondering if there is a class that handles these cases so I just use that and don't have to worry about where to save it.
  • n.evermind
    n.evermind over 12 years
    Have a look at developer.apple.com/library/ios/#documentation/DataManagemen‌​t/… which gives some sample code to determine if something should be put in the local sandbox or the cloud.
  • earnshavian
    earnshavian over 12 years
    Thanks for that. I'd seen that doc but earlier in my iCloud mission so I had forgotten the code it offers. I'm going to try to adapt your sample to support local and remote. I'm still not clear how we handle the user who disables iCloud since we lose the ubiquitous URL but I'll have a crack and share an update.
  • n.evermind
    n.evermind over 12 years
    So in a way, it's a bit stupid that we have to use URLs for the cloud and PATHs for the local sandbox. It would be nice if iCloud could handle everything for us... but this way, we basically need to code two different methods for every file we open.
  • n.evermind
    n.evermind over 12 years
    I just re-read your post. I am now saving the user's preference (i.e. user wants/doesn't want to use iCloud) in NSUserDefaults. This is what Apple suggests too. I always check if iCloud is accessible. If it is not accessible, I tell the users to turn it on - but only if they have not explicitly told the app that they do not wish to use it. Otherwise it gets annoying for those who do not wish to use iCloud. Once I have determined if iCloud is enabled, I will either follow the ubiquitous URL route and use UIDocument OR will simply open the files from the sandbox as in the good old days.
  • ngb
    ngb about 10 years
    url link is broken...