12

I want to have my app save the documents it creates to iCloud Drive, but I am having a hard time following along with what Apple has written. Here is what I have so far, but I'm not for sure where to go from here.

UPDATE2

I have the following in my code to manually save a document to iCloud Drive:

- (void)initializeiCloudAccessWithCompletion:(void (^)(BOOL available)) completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.ubiquityURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
        if (self.ubiquityURL != nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"iCloud available at: %@", self.ubiquityURL);
                completion(TRUE);
            });
        }
        else {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"iCloud not available");
                completion(FALSE);
            });
        }
    });
}
if (buttonIndex == 4) {



     [self initializeiCloudAccessWithCompletion:^(BOOL available) {

        _iCloudAvailable = available;

        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

        NSString *documentsDirectory = [paths objectAtIndex:0];

        NSString *pdfPath = [documentsDirectory stringByAppendingPathComponent:selectedCountry];

        NSURL* url = [NSURL fileURLWithPath: pdfPath];


        [self.manager setUbiquitous:YES itemAtURL:url destinationURL:self.ubiquityURL error:nil];


    }];

       }

I have the entitlements set up for the App ID and in Xcode itself. I click the button to save to iCloud Drive, and no errors pop up, the app doesn't crash, but nothing shows up on my Mac in iCloud Drive. The app is running on my iPhone 6 Plus via Test Flight while using iOS 8.1.1.

If I run it on Simulator (I know that it won't work due to iCloud Drive not working with simulator), I get the crash error: 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[3]'

user717452
  • 33
  • 14
  • 73
  • 149
  • `setUbiquitous:itemAtURL:destinationURL:error:` returns a BOOL whether the operation was successful or not. What is the return value? Also provide a pointer to an `NSError` object for the `error` parameter. If the method returns NO, this will probably give you some more guidance. – fguchelaar Dec 07 '14 at 22:18
  • @fguchelaar sorry I have never messed much with NSFileManager so some of this is going over my head. – user717452 Dec 08 '14 at 01:14

2 Answers2

42

Well, you've got me interested in this matter myself and as a result I've spent way to much time on this question, but now that I've got it working I hope it helps you as well!

File on my Mac

To see what actually happens in the background, you can have a look at ~/Library/Mobile Documents/, as this is the folder where the files eventually will show up. Another very cool utility is brctl, to monitor what happens on your mac after storing a file in the iCloud. Run brctl log --wait --shorten from a Terminal window to start the log.

First thing to do, after enabling the iCloud ability (with iCloud documents selected), is provide information for iCloud Drive Support (Enabling iCloud Drive Support). I also had to bump my bundle version before running the app again; took me some time to figure this out. Add the following to your info.plist:

<key>NSUbiquitousContainers</key>
<dict>
    <key>iCloud.YOUR_BUNDLE_IDENTIFIER</key>
    <dict>
        <key>NSUbiquitousContainerIsDocumentScopePublic</key>
        <true/>
        <key>NSUbiquitousContainerSupportedFolderLevels</key>
        <string>Any</string>
        <key>NSUbiquitousContainerName</key>
        <string>iCloudDriveDemo</string>
    </dict>
</dict>

Next up, the code:

- (IBAction)btnStoreTapped:(id)sender {
    // Let's get the root directory for storing the file on iCloud Drive
    [self rootDirectoryForICloud:^(NSURL *ubiquityURL) {
        NSLog(@"1. ubiquityURL = %@", ubiquityURL);
        if (ubiquityURL) {

            // We also need the 'local' URL to the file we want to store
            NSURL *localURL = [self localPathForResource:@"demo" ofType:@"pdf"];
            NSLog(@"2. localURL = %@", localURL);

            // Now, append the local filename to the ubiquityURL
            ubiquityURL = [ubiquityURL URLByAppendingPathComponent:localURL.lastPathComponent];
            NSLog(@"3. ubiquityURL = %@", ubiquityURL);

            // And finish up the 'store' action
            NSError *error;
            if (![[NSFileManager defaultManager] setUbiquitous:YES itemAtURL:localURL destinationURL:ubiquityURL error:&error]) {
                NSLog(@"Error occurred: %@", error);
            }
        }
        else {
            NSLog(@"Could not retrieve a ubiquityURL");
        }
    }];
}

- (void)rootDirectoryForICloud:(void (^)(NSURL *))completionHandler {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSURL *rootDirectory = [[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil]URLByAppendingPathComponent:@"Documents"];

        if (rootDirectory) {
            if (![[NSFileManager defaultManager] fileExistsAtPath:rootDirectory.path isDirectory:nil]) {
                NSLog(@"Create directory");
                [[NSFileManager defaultManager] createDirectoryAtURL:rootDirectory withIntermediateDirectories:YES attributes:nil error:nil];
            }
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(rootDirectory);
        });
    });
}

- (NSURL *)localPathForResource:(NSString *)resource ofType:(NSString *)type {
    NSString *documentsDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *resourcePath = [[documentsDirectory stringByAppendingPathComponent:resource] stringByAppendingPathExtension:type];
    return [NSURL fileURLWithPath:resourcePath];
}

I have a file called demo.pdf stored in the Documents folder, which I'll be 'uploading'.

I'll highlight some parts:

URLForUbiquityContainerIdentifier: provides the root directory for storing files, if you want to them to show up in de iCloud Drive on your Mac, then you need to store them in the Documents folder, so here we add that folder to the root:

NSURL *rootDirectory = [[[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil]URLByAppendingPathComponent:@"Documents"];

You also need to add the file name to the URL, here I copy the filename from the localURL (which is demo.pdf):

// Now, append the local filename to the ubiquityURL
ubiquityURL = [ubiquityURL URLByAppendingPathComponent:localURL.lastPathComponent];

And that's basically it...

As a bonus, check out how you can provide an NSError pointer to get potential error information:

// And finish up the 'store' action
NSError *error;
if (![[NSFileManager defaultManager] setUbiquitous:YES itemAtURL:localURL destinationURL:ubiquityURL error:&error]) {
    NSLog(@"Error occurred: %@", error);
}
fguchelaar
  • 4,779
  • 23
  • 36
  • It looks like I'm sending my file (I get an error: file already exists when I try to write the same file many times), and I'm seeing stuff in `brutal log` on my Mac, but I don't see the folder on iCloud.com or on my Mac. Any thoughts why this could be the case? – Tim Arnold Jan 14 '15 at 20:27
  • 4
    EDIT: Had to increment `CFBundleVersion`. Maddening! http://stackoverflow.com/a/25328864/1148702 – Tim Arnold Jan 14 '15 at 20:45
  • I know, madness.. I'll highlight that bit it my answer some more, you seem to have overlooked it :) – fguchelaar Jan 14 '15 at 21:33
  • Thanks, too bad I missed that (found it in another answer). Another bit that I think would make this answer better: don't rely on some random `demo.pdf` to be in the local `Documents` directory. I added a bit of code to just write a small text file to the local `Documents` directory, and that makes this sample more self-contained. Thanks again! – Tim Arnold Jan 15 '15 at 14:15
  • @fguchelaar I exactly did everything you explained. still I can't see my app's folder in iCloud Drive folder or icloud.com. Can you take a look my problem? http://stackoverflow.com/questions/30385940/why-my-app-is-not-shown-in-icloud-drive-folder – amone May 23 '15 at 14:14
  • 1
    How is to retrieve a file from the iCloud Drive? – user3065901 Jun 15 '15 at 15:24
  • I can't quite get this working. I don't get any error messages at all, but the document doesn't appear in the iCloud drive. Here's my code - does anyone know what I've done wrong? https://github.com/npomfret/react-native-cloud-fs/blob/master/ios/RNCloudFs.m#L16 – pomo Nov 07 '16 at 18:00
  • I tried the code. Each time it says "The file “test” couldn’t be saved in the folder “test” because a file with the same name already exists." But no folder no file is visible on my iCloud drive. Please suggest how can I upload entire folder to iCloud drive and retrieve it back? Also how can I see my folder and files in iCloud drive? – Bhavna Mar 23 '18 at 13:19
  • 1
    Perhaps it was obvious but I missed it the first time around... If you're using a custom iCloud container, you must use that in place of iCloud.YOUR_BUNDLE_IDENTIFIER. – ibuprofane Sep 08 '18 at 16:37
2

If you are intending to work with UIDocument and iCloud, this guide from Apple is pretty good: https://developer.apple.com/library/ios/documentation/DataManagement/Conceptual/UsingCoreDataWithiCloudPG/Introduction/Introduction.html

EDITED: Don't know of any better guide of hand, so this may help:

You will need to fetch the ubiquityURL using the URLForUbuiquityContainerIdentifier function on NSFileManager (which should be done asynchronously). Once that is done, you can use code like the following to create your document.

NSString* fileName = @"sampledoc";
NSURL* fileURL = [[self.ubiquityURL URLByAppendingPathComponent:@"Documents" isDirectory:YES] URLByAppendingPathComponent:fileName isDirectory:NO];

UIManagedDocument* document = [[UIManagedDocument alloc] initWithFileURL:fileURL];

document.persistentStoreOptions = @{
                    NSMigratePersistentStoresAutomaticallyOption : @(YES),
                    NSInferMappingModelAutomaticallyOption: @(YES),
                    NSPersistentStoreUbiquitousContentNameKey: fileName,
                    NSPersistentStoreUbiquitousContentURLKey: [self.ubiquityURL URLByAppendingPathComponent:@"TransactionLogs" isDirectory:YES]
};

[document saveToURL:fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {

}];

You'll also want to look into using NSMetadataQuery to detect documents uploaded from other devices and potentially queue them for download, and observing the NSPersistentStoreDidImportUbiquitousContentChangesNotification to find about changes made via iCloud, among other things.

** Edit 2 **

Looks like you are trying to save a PDF file, which is not quite what Apple considers a "document" in terms of iCloud syncing. No need to use UIManagedDocument. Remove the last 3 lines of your completion handler and instead just use NSFileManager's setUbiquitous:itemAtURL:destinationURL:error: function. The first URL should be a local path to the PDF. The second URL should be the path within the ubiquiuty container to save as.

You may also need to look into NSFileCoordinator perhaps. I think this guide from Apple may be the most relevant: https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/iCloud/iCloud.html

zeroimpl
  • 2,746
  • 22
  • 19