2

I'm seeking advice on using iCloud in a limited way. The docs are focused on syncing between multiple devices, which I do not need. When I say iCloud, I'm also including iCloud Drive, if that matters.

My iPhone app stores its data and state in a single SQLite database file that I want to save to iCloud (or iCloud Drive) for the purpose of backup, not for syncing changes between multiple devices. At launch, SQLite opens the database file and uses it synchronously (I'm using FMDB). The database file should remain on the device so, if iCloud is down, SQLite won't know or care. iCloud would have a shadow copy.

The iCloud copy does not always need to be current -- I'm okay with programmatically initiating an update to iCloud, and the restore if necessary. If the local database file becomes corrupted or if the user deletes the app, I would restore the file from iCloud. The file would usually be smaller than 1 MB, but might be large (100 MB), but mostly invariant.

I'm not syncing because of the data integrity risks of sharing whole SQLite files, and I cannot convert to Core Data (it's a huge app using FMDB).

I know that the iOS does daily backups to iCloud, and that would be sufficient if I could restore just my app from iCloud (and not the entire device).

I need advice on using iCloud for a shadow backup and restore, but not syncing.

UPDATE: I have made progress on this issue; I can save and restore my real SQLite file to/from iCloud. I subclassed UIDocument with these two overrides:

@implementation MyDocument

// Override this method to return the document data to be saved.
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
    NSString *databasePath = ... // path to my SQLite file in Documents;
    NSData *dbData = [NSData dataWithContentsOfFile: databasePath];
    return dbData; // the entire database file
}

// Override this method to load the document data into the app’s data model.
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
    NSString *databasePath = ... // path to my SQLite file in Documents;
    NSData *dbData = contents;
    BOOL ret = [dbData writeToFile: databasePath atomically:YES];
    return YES;
}

@end

My SQLite database file is in Documents and it stays there, but I write the shadow copy to iCloud thusly:

- (void)sendToICloud
{
    NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];

    NSString *iCloudFileName = @"..."; // a file name I choose
    NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:@"Documents"]
                                URLByAppendingPathComponent:iCloudFileName];

    MyDocument *myDoc = [[MyDocument alloc] initWithFileURL:ubiquitousPackage];

    [myDoc saveToURL:[myDoc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
        if ( success ) {
            [myDoc closeWithCompletionHandler:^(BOOL success) {
            }];
        } else {
            NSLog(@"iCloud write Syncing FAILED with iCloud");
        }
    }];
}

Now my issues are:

  1. I have created a custom container in Xcode that appears in my entitlements file and in the Dev portal for the App ID. I can save documents to iCloud from either of my two devices (iPhone and iPad). But, my two devices do not see each other's files. There are numerous old threads about this with random solutions, but nothing has solved the problem.

  2. The iCloud documentation says files are updated incrementally, but I don't know if that applies in my case. If I use a new filename for MyDocument (my iCloudFileName, above), I realize that an entire new file will be sent to iCloud. But, if I reuse the previous filename and send an updated NSData each time (as I'm doing in loadFromContents:ofType:error:), will iOS only send the parts that have changed since the last time I saved the file?

Jeff
  • 2,659
  • 1
  • 22
  • 41

1 Answers1

0

Another subject line now would be: Using iCloud for file backup across multiple devices.

I've solved my primary issue of seeing iCloud data sent by multiple devices. According to WWDC 2015 "Building Document Based App," to see all files (even those from other devices), use NSMetadataQuery, not NSFileManager. Also, the video presentation includes a warning about using NSURL because documents might be moved (use Bookmarks), but that doesn't affect my use case.

I still don't know if sending partially-changed NSData will cause incremental updates.

My UIDocument overrides and my sendToICloud method remain the same. What's new is my iCloud search.

If you aren't writing to the default iCloud Documents directory, writing will fail if the directory does not exist. It's easier to just create it every time, thusly:

- (void)writeToiCloud
{
    // ...
#define UBIQUITY_ID         @"iCloud.TEAMID.bundleID.appName"
    NSFileManager *defaultManager = [NSFileManager defaultManager];
    NSURL *ubiq = [defaultManager URLForUbiquityContainerIdentifier:UBIQUITY_ID];
    NSURL *ubiquitousFolder = [ubiq URLByAppendingPathComponent:@"MyFolder"];
    [defaultManager createDirectoryAtURL:ubiquitousFolder
             withIntermediateDirectories:YES
                              attributes:nil error:nil];
    // ...
    // ... forSaveOperation:UIDocumentSaveForCreating ...
}

- (void)updateFileList
{
    // Use an iVar so query stays in scope
    // Instance variables maintain a strong reference to the objects by default
    query = [[NSMetadataQuery alloc] init];
    [query setSearchScopes:@[ // use both for testing, then only DocumentsScope
                             NSMetadataQueryUbiquitousDataScope, // not in Documents
                             NSMetadataQueryUbiquitousDocumentsScope, // in Documents
                             ]
     ];
    // add a predicate if desired to restrict the search.
    // No predicate finds everything, but Apple says:
    // a query can’t be started if ... no predicate has been specified.

    NSPredicate *predicate = [NSPredicate predicateWithFormat: @"%K like '*'", NSMetadataItemFSNameKey];
    // NSPredicate *predicate = [NSPredicate predicateWithFormat: @"%K CONTAINS %@", NSMetadataItemPathKey, @"/MyFolder/"];
    // NSPredicate *predicate = [NSPredicate predicateWithFormat: @"%K CONTAINS %@", NSMetadataItemPathKey, @"/Documents/"];
    [query setPredicate:predicate];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(queryDidFinish:)
                                                 name:NSMetadataQueryDidFinishGatheringNotification
                                               object:query];
    [query startQuery];

}

- (void)queryDidFinish:(NSNotification *)notification
{
    // iVar: NSMetadataQuery *query
    for ( NSMetadataItem *item in [query results] ) {
        NSURL *itemURL = [item valueForAttribute:NSMetadataItemURLKey];
        NSLog(@"fileName: %@",itemURL.lastPathComponent);

        // can't use getResourceValue:forKey:error
        //   that applies to URLs that represent file system resources.

        if ( [itemURL.absoluteString hasSuffix:@"/"] ) {
            continue; // skip directories
        }

        // process the itemURL here ...
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        [self.tableView reloadData];
    });

    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NSMetadataQueryDidFinishGatheringNotification
                                                  object:query];
}
Jeff
  • 2,659
  • 1
  • 22
  • 41