35
votes

I already have an iPhone App that stores data in a file in the local documents folder. Now I learnt about iCloud technologies and my first question was: is there a way to use iCloud as a directory when sometimes I check for new versions?

I mean: can I avoid using UIDocument, file coordinators and file presenters? I want just to know if could treat iCloud like a special folder and only use NSFileManager to push and retrieve files.

Last note: I don't use Core Data or any database, I only have a data file.

Edit:

I already read the official Apple iCloud documentation so don't link me to them. I only need some code examples.

5

5 Answers

26
votes

What "works" for me is just simple:

NSFileManager *fm = [NSFileManager defaultManager];

NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

if (ubiq == nil) {
    return NO;
}

NSError *theError = nil;

[fm setUbiquitous:true itemAtURL:backupUrl destinationURL:[[ubiq URLByAppendingPathComponent:@"Documents" isDirectory:true] URLByAppendingPathComponent:backupName] error:&theError];

Apple says to call on the non-UI thread. Having the files "moved". You can query for them via NSMetaDataQuerylike this:

self.query = [[NSMetadataQuery alloc] init];
[self.query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *pred = [NSPredicate predicateWithFormat: @"%K like '*.db'", NSMetadataItemFSNameKey];
[self.query setPredicate:pred];
[[NSNotificationCenter defaultCenter] addObserver:self 
                                         selector:@selector(queryDidFinishGathering:) 
                                             name:NSMetadataQueryDidFinishGatheringNotification 
                                           object:self.query];

[self.query startQuery];

- (void)queryDidFinishGathering:(NSNotification *)notification {
    NSMetadataQuery *query = [notification object];
    [query disableUpdates];
    [query stopQuery];

    [self loadData:query];

    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];

    self.query = nil; 
}

Sample of enumeration through the query results:

- (void)loadData:(NSMetadataQuery *)query {
    [self.backups removeAllObjects];

    for (NSMetadataItem *item in [query results]) {
        NSURL *url = [item valueForAttribute:NSMetadataItemURLKey];
        [self.backups addObject:url.lastPathComponent];
    }

    [_table reloadData];

    [self.loadingBackupIndicator stopAnimating];
    self.loadingIndicatorLabel.text = [NSString stringWithFormat: @"%d backups found", [self.backups count]];
}

And to start "download" of the concrete file:

NSFileManager *fm = [NSFileManager defaultManager];

NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

if (ubiq == nil) {
    return NO;
}

NSError *theError = nil;

bool started = [fm startDownloadingUbiquitousItemAtURL:[[ubiq URLByAppendingPathComponent:@"Documents" isDirectory:true] URLByAppendingPathComponent:backupName] error:&theError];

NSLog(@"started download for %@ %d", backupName, started);

if (theError != nil) {
    NSLog(@"iCloud error: %@", [theError localizedDescription]);
}

With checks for file "being downloaded":

- (BOOL)downloadFileIfNotAvailable {
    NSNumber *isIniCloud = nil;

    NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

    NSURL *file = [[ubiq URLByAppendingPathComponent:@"Documents" isDirectory:true] URLByAppendingPathComponent:self.backupName];

    if ([file getResourceValue:&isIniCloud forKey:NSURLIsUbiquitousItemKey error:nil]) {
        // If the item is in iCloud, see if it is downloaded.
        if ([isIniCloud boolValue]) {
            NSNumber*  isDownloaded = nil;
            if ([file getResourceValue:&isDownloaded forKey:NSURLUbiquitousItemIsDownloadedKey error:nil]) {
                if ([isDownloaded boolValue]) {
                    [self.loadingBackupIndicator stopAnimating];
                    self.loadingIndicatorLabel.text = @"Downloaded";

                    ....

                    [[NSFileManager defaultManager] copyItemAtPath:[file path] toPath:restorePath error:&theError ];

                    ....

                    return YES;
                }

                self.loadingCheckTimer = [NSTimer timerWithTimeInterval:3.0f target:self selector:@selector(downloadFileIfNotAvailable) userInfo:nil repeats:NO];
                [[NSRunLoop currentRunLoop] addTimer:self.loadingCheckTimer forMode:NSDefaultRunLoopMode];

                return NO;
            }
        }
    }

    return YES;
}

I didn't expect the code to be that long and sorry for providing very raw snippets here. No intent to say the above can be a production quality of code, just sharing the concept.

I have not yet submitted that inside my app to Apple, so can't tell that would be "approved" to the app store (if they find or care...)

17
votes

I know how you feel, iCloud is a bit daunting. However, I think there is no way around UIDocument, file coordinators etc. and to simply use iCloud as a simple folder.

If you are looking for an easy to understand sample code, please have a look at this post:

iCloud basics and code sample

I included a full sample code which covers the bare minimums of iCloud and pretty much uses it like a directory. Perhaps this makes it less daunting for you to use UIDocument, file coordinators etc.

But, like you, I wish there was an easier and more compatible way with the good old documentary folder idea. However, as this is iCloud and as iCloud does several things more (like keeping everything in sync on different devices, constantly updating to cloud etc.), there will be no way around UIDocument etc.

14
votes

You can upload individual files to iCloud using NSFileManager. I posted a complete walkthrough on how to do that on my blog, but here's the relevant NSFileManager code:

NSURL *destinationURL = [self.ubiquitousURL URLByAppendingPathComponent:@"Documents/image.jpg"]
[[NSFileManager defaultManager] setUbiquitous:YES 
                                    itemAtURL:sourceURL
                               destinationURL:destinationURL
                                        error:&error]
8
votes

There isn't really a way around using UIDocument. I tried to do it in one of my first uses of iCloud, but it turned out to be a disaster without UIDocument. Using UIDocument at first seems like a lot of extra work, but it isn't.

You can easily subclass UIDocument in under an hour and get it to work with any type of file (just set the content property as NSData). It also provides numerous benefits over the standard file system:

  • Change tracking
  • File conflict resolution
  • Document state support
  • Enhanced save / open / close features

Honestly, spending just an hour or two reading over the Apple documentation and then using it is well worth the time and brain power. A good starter article on iCloud Document storage can be found in Apple's Developer Documentation.


I have written a UIDocument subclass that will work with any type of file (NSData specifically). You can view, download, and modify the code for the UIDocument subclass on GitHub.

Create the document:

// Initialize a document with a valid file path
iCloudDocument *document = [[iCloudDocument alloc] initWithFileURL:fileURL];

// Set the content of the document
document.contents = content;

// Increment the change count
[document updateChangeCount:UIDocumentChangeDone];

Save an existing document:

// Save and close the document
[document closeWithCompletionHandler:nil];

Save a new document:

[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:nil];

You can also sync all files stored within iCloud by using NSMetadataQuery. Apple provides a very nice example of using NSMetadata query to sync app files. Also make sure to check for iCloud before performing these operations (hint: use the ubiquityIdentityToken method on NSFileManager).


You may also want to consider using an open-source library such as iCloud Document Sync. The iCloud Document Sync project makes it very easy to store and sync app files:

Integrate iCloud into iOS document projects with one-line code methods. Sync, upload, manage, and remove documents from iCloud quickly and easily. Helps to make iCloud "just work" for developers too.

In almost every iCloud Document Sync method, all you have to do is pass in your file data as a parameter and then it handles the rest (saving, syncing, etc.).

DISCLAIMER: I am a contributing developer to the open-source project, iCloud Document Sync. However, I believe that this project will be beneficial to you, and is relevant to this question. This is not a promotion or advertisement.

-1
votes

Use this code:

+ (NSString *)Pathsave {
    NSString *os5 = @"5.0";

    NSString *currSysVer = [[UIDevice currentDevice] systemVersion];
    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];

    if ([currSysVer compare:os5 options:NSNumericSearch] == NSOrderedAscending) {
       // Lower than 4
        return path;
    } else if ([currSysVer compare:os5 options:NSNumericSearch] == NSOrderedDescending) {
        // 5.0.1 and above        
        return path;
    } else {
        // iOS 5
        path = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Caches"];
        return path;
    }

    return nil;
}