iCloud and UIDocument: Beyond the Basics, Part 3/4

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. Welcome back to our document-based iCloud app tutorial series! In this tutorial series, we are making a complete document-based iCloud app called PhotoKeeper, with features that go beyond just the basics. In the first and second parts of the […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Querying iCloud Files

As we mentioned in part 1, you can’t just enumerate the iCloud directory with NSFileManager APIs like you do for the documents directory. Instead, you have to use a fancy API called NSMetadataQuery.

This API lets you set up a search on a directory, such as “give me all files that end with PTK”. It will return the initial results, and then continue to watch the directory for you, sending you update notifications as they come in. It’s pretty handy and efficient in comparison to the alternative (periodic polling).

Let’s see what it looks like. Go to the “iCloud Query” section and add this right above startQuery:

- (NSMetadataQuery *)documentQuery {
    
    NSMetadataQuery * query = [[NSMetadataQuery alloc] init];
    if (query) {
        
        // Search documents subdir only
        [query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
        
        // Add a predicate for finding the documents
        NSString * filePattern = [NSString stringWithFormat:@"*.%@", PTK_EXTENSION];
        [query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE %@",
                             NSMetadataItemFSNameKey, filePattern]];        
        
    }
    return query;
    
}

When you create a NSMetadataQuery, you can tell it to watch the “Documents” subdirectory of the iCloud root (this is where we’ll be storing our files) by passing in NSMetadataQueryUbiquitousDocumentsScope as the search scope.

You can then filter the output with a predicate. We set up one here to look for files that end with the PTK extension.

Next let’s create some methods to start and stop this query. Make the following changes to PTKMasterViewController.m:

// Add new private instance variables
NSMetadataQuery * _query;
BOOL _iCloudURLsReady;
NSMutableArray * _iCloudURLs;

// Add to "iCloud query" section after documentQuery method, replacing the existing startQuery method
- (void)stopQuery {
    
    if (_query) {
        
        NSLog(@"No longer watching iCloud dir...");
        
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:nil];
        [[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidUpdateNotification object:nil];
        [_query stopQuery];
        _query = nil;
    }
    
}

- (void)startQuery {
    
    [self stopQuery];
    
    NSLog(@"Starting to watch iCloud dir...");
    
    _query = [self documentQuery];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(processiCloudFiles:)
                                                 name:NSMetadataQueryDidFinishGatheringNotification
                                               object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(processiCloudFiles:)
                                                 name:NSMetadataQueryDidUpdateNotification
                                               object:nil];
    
    [_query startQuery];
}

// Add in viewDidLoad before call to refresh
_iCloudURLs = [[NSMutableArray alloc] init];

// Add at beginning of refresh method
_iCloudURLsReady = NO;
[_iCloudURLs removeAllObjects];

startQuery gets the query and starts it up. It also registers for two notifications – the initial gather notification, as well as further updates. In both cases, processiCloudFiles will be called – we’ll write that next. stopQuery just does the opposite and tears things down.

Continue by adding the implementation of processiCloudFiles:

- (void)processiCloudFiles:(NSNotification *)notification {
    
    // Always disable updates while processing results
    [_query disableUpdates];
    
    [_iCloudURLs removeAllObjects];
    
    // The query reports all files found, every time.
    NSArray * queryResults = [_query results];
    for (NSMetadataItem * result in queryResults) {
        NSURL * fileURL = [result valueForAttribute:NSMetadataItemURLKey];
        NSNumber * aBool = nil;
        
        // Don't include hidden files
        [fileURL getResourceValue:&aBool forKey:NSURLIsHiddenKey error:nil];
        if (aBool && ![aBool boolValue]) {
            [_iCloudURLs addObject:fileURL];
        }        
        
    }        
    
    NSLog(@"Found %d iCloud files.", _iCloudURLs.count);
    _iCloudURLsReady = YES;
    
    if ([self iCloudOn]) {
        
        // Remove deleted files
        // Iterate backwards because we need to remove items form the array
        for (int i = _objects.count -1; i >= 0; --i) {
            PTKEntry * entry = [_objects objectAtIndex:i];
            if (![_iCloudURLs containsObject:entry.fileURL]) {
                [self removeEntryWithURL:entry.fileURL];
            }
        }
        
        // Add new files
        for (NSURL * fileURL in _iCloudURLs) {                
            [self loadDocAtURL:fileURL];        
        }
        
        self.navigationItem.rightBarButtonItem.enabled = YES;
        
    } 
        
    [_query enableUpdates];
    
}

When you’re processing the results of an NSMetadataQuery, it’s important to stop the query so you don’t get more updates while you’re in the middle of processing the first, so we do that at the beginning and end of the method.

We then loop through the query results, and add everything to the list except for items that are hidden. We add all of the results to the most up-to-date list of _iCloudURLs, which we will find handy later, and set a flag indicating that they are ready.

If iCloud isn’t on, that’s all we do. But if it is on, we make sure our local list of entries matches the list of iCloud files. We first loop through our entries and remove anything not in the list of iCloud files by calling removeEntryWithURL. We also add anything that isn’t already in the list to the list by calling loadDocAtURL. When this is all done it’s safe to add a new document again, so we enable the right bar button item.

Next, add these methods right after viewDidUnload:

- (void)viewWillAppear:(BOOL)animated {
    [_query enableUpdates];
}

- (void)viewWillDisappear:(BOOL)animated {
    [_query disableUpdates];
}

This is important to add because UIDocument has issues if you try to open more than instance at a time pointing to the same fileURL, so we don’t want our table view trying to refresh itself (hence loading a UIDocument to get its medatata) while we’re editing a document.

We could run the app now but there wouldn’t be any use, as there’s no way to create a file in iCloud yet. Luckily, since we already laid most of the groundwork by using UIDocument this is quite easy to fix! Simply replace getDocURL with the following:

- (NSURL *)getDocURL:(NSString *)filename {    
    if ([self iCloudOn]) {
        NSURL * docsDir = [_iCloudRoot URLByAppendingPathComponent:@"Documents" isDirectory:YES];
        return [docsDir URLByAppendingPathComponent:filename];
    } else {
        return [self.localRoot URLByAppendingPathComponent:filename];    
    }
}

Here we’ve implemented the “iCloudOn” case to return the Documents subdirectory of the iCloud root, where we will be saving our files.

When we go to create a new file, now it will be creating it in the iCloud directory, so the daemon will pick up on it and synchronize it. Everything else will just work since we’re using UIDocument!

Compile and run on your device, and make sure iCloud is available and switched on. Create some documents and verify that they are created in the iCloud directory by looking at the logs.

Now for the fun part – start up a second device and you should see the same files pull down from iCloud! :D

iCloud Synchronizing across 2 Devices

If you have any troubles, try deleting the app from your device and re-installing, double checking that your Document exports are set up properly, or compare your project to the solution at the end of this tutorial.

Contributors

Over 300 content creators. Join our team.