How to use iCloud to store and sync app files

43,227

Solution 1

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.

Solution 2

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...)

Solution 3

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]

Solution 4

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.

Share:
43,227
albianto
Author by

albianto

I'm not Mac friendly, I'm Mac only

Updated on March 23, 2020

Comments

  • albianto
    albianto about 4 years

    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.

  • albianto
    albianto over 12 years
    I agree, if you say there's no way, I will read the tutorial. But I thought there should have been an easier way to do that, like when you write or read a file in the app sandbox. Thanks
  • Stanislav Dvoychenko
    Stanislav Dvoychenko over 12 years
    just to note that a "production" version of a code above passed it inside the app to the app store.
  • Gypsa
    Gypsa about 12 years
    can you please provide a sample project including the above code.Please help me if it is possible.
  • Bajaj
    Bajaj about 11 years
    @StanislavDvoychenko hello sir give me some idea about icloud coding part .means how to strore and retrive etc. plz .thanks in advance
  • albianto
    albianto over 10 years
    That's exactly what I did in the end.
  • SafeFastExpressive
    SafeFastExpressive about 10 years
    NSURLUbiquitousItemIsDownloadedKey has been deprecated in iOS7 and replaced with NSURLUbiquitousItemDownloadingStatusKey. I'd also like to say thanks to Stanislav for posting such a complete and useful example.
  • pronebird
    pronebird over 8 years
    If your use case is to dump data into iCloud, you can do that with file coordinator and there is no need for UIDocument.
  • jjxtra
    jjxtra almost 8 years
    Is there a way to copy instead of move?
  • Govind
    Govind over 7 years
    can we able to share a link via iCloud? Like that in Pages app and Key not app. How the file permissions and all can be set and share the link via email
  • coolcool1994
    coolcool1994 over 7 years
    When I do this I tried fetching the file in the iCloud using residentData = [[NSMutableArray alloc] initWithContentsOfURL:icloudURL]; but this returns nil every time..