How to detect file changes in your ubiquitous iCloud Folder

If Device A saves or changes a file in your app’s iCloud folder, Device B might want to know about this. So you’d imagine there should be an easy way to monitor those changes by way of a notification or a delegate. Sadly there isn’t. Like many things in Objective C, this is way more complicated than it needs to be.

Sure, we could use UIDocument which has those notifications built in – but that adds a layer of complexity to the simple act of saving a frigging file that many apps seriously don’t need.

The good news is that we can monitor file changes to any file in our ubiquitous folder by executing an NSMetadataQuery. It feels like a hack but it works. Let me explain.

An NSMetadataQuery is really a way to search files in a folder and return results. This includes iCloud documents. As a byproduct it sends notifications if there have been changes to those results since the last query. Queries are ongoing until stopped and are carried out once every second.

So to monitor changes to files in our iCloud folder, we need to do the following:

  • create an NSMetadataQuery, monitoring all files in our ubiquitous Documents folder (including new save, overwrite and delete)
  • add an observer for the query
  • create a method that gets fired via the observer

Let’s do this:

Give your class a property of NSMetadataQuery and use this custom initialiser to kick it off:

- (NSMetadataQuery *)query {
    if (!_query) {
        _query = [[NSMetadataQuery alloc]init];
        
        NSArray *scopes = @[NSMetadataQueryUbiquitousDocumentsScope];
        _query.searchScopes = scopes;
        
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@", NSMetadataItemFSNameKey, @"*"];
        _query.predicate = predicate;
        
        if (![_query startQuery]) {
            NSLog(@"Query didn't start... for whatever reason");
        }
    }
    return _query;
}

The query needs a search scope and a predicate to work. We also need to start it. The initialiser does all that. Next we need an observer. Add it under viewDidLoad:

// observer to refresh image on iCloud change via NSMetadataQuery
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(updateImage) name:NSMetadataQueryDidUpdateNotification object:self.query];

Note that we have to specify the object to observe. Last but not least we need a method that is called when data changes. I’ll call mine updateImage:

- (void)updateImage {
    
    // called when iCloud sends change notification
    self.imageView.image = [self loadFromCloudDocuments];
}

Another puzzle solved!

Here’s a demo project I’ve created which uses this technique:





One thought on “How to detect file changes in your ubiquitous iCloud Folder

  1. A common (related) problem in this scenario is that Device A uploads data and displays it correctly, but Device B is unable to do so – even though the file appears to be present. This happens because even though the file wrapper has downloaded from iCloud, the file contents has not.

    To fix this, implicitly download the file before using it:

    [[NSFileManager defaultManager]startDownloadingUbiquitousItemAtURL:cloudURL error:nil];
    

Leave a Reply