Back Up and Restore with Dropbox - Learning Core Data for iOS (2014) 

Learning Core Data for iOS (2014)

13. Back Up and Restore with Dropbox

Information is not knowledge.

Albert Einstein

In Chapter 12, “Search,” the search functionality was added to Grocery Dude. The feature set will now be extended to include a data backup and restore capability. This capability will be achieved by integrating with Dropbox, which is a free web-service providing limited cloud-based storage. The Dropbox Sync API for iOS (www.dropbox.com/developers/sync) will be leveraged to provide a device-local Dropbox file system. Anything copied to this file system will be automatically synchronized with the user’s Dropbox account. If Grocery Dude users don’t have a Dropbox account, they will need to create one. This chapter will demonstrate the entire process of creating and restoring backup ZIP files, along with the transfer of those files to and from Dropbox. The restore process will include steps to reload the persistent store once a restore has been completed.

The intent of this chapter should not be confused with database synchronization. Rather, this chapter will demonstrate how to back up and restore a Core Data persistent store. If you instead need to synchronize a persistent store between a single user’s iOS devices, the upcoming iCloud chapters are more appropriate. If you need to synchronize a persistent store between devices of multiple users, Chapter 16, “Web Service Integration,” is more appropriate.

To back up Core Data, everything in the applicationStoresDirectory needs to be preserved. When Grocery Dude launches, you can see in the console log that this is where Grocery-Dude.sqlite is located. When backing up a persistent store file, it is important to be aware that there are other critical files alongside it, including some hidden ones. These files need to be preserved in addition to the SQLite file. This is due to the WAL journaling mode and the Allows External Storage entity attribute setting. In addition, it may be possible that other stores will be added in the future; therefore, backing up the entire Stores directory is a good catchall for future enhancements.

When a backup is taken, a ZIP file containing a copy of the entire Stores directory will be created. Creating a ZIP file is a great way to store a backup because it takes up less space than the original files and makes them more portable. Once created, the ZIP file will be moved to the local Dropbox cache, which automatically synchronizes with the Dropbox web service. Figure 13.1 shows an overview of this process.

Image

Figure 13.1 Data backup with Dropbox

Dropbox Integration

When an application is integrated with Dropbox, a local cache of the linked user’s Dropbox file system will be accessible. Dropbox-integrated applications using the Sync API can be constrained to their own subfolder within the Dropbox Apps directory. For example, Apps/Grocery Dude/ will be the root folder visible to Grocery Dude. Everything within this cached folder will be synchronized to the Dropbox web service automatically by the Dropbox framework. If you don’t already have a Dropbox account, you’ll need to create one at www.dropbox.com.

To integrate an application with Dropbox, you’ll need to configure a new application on the Dropbox portal. The application name needs to be unique, and the name “Grocery Dude” is already taken. You’ll need to use something along the lines of “Grocery Dude by Your Name” to ensure your own application is unique.

Create a new Dropbox API app as follows:

1. Access https://www.dropbox.com/developers/apps using your Dropbox account.

2. Create a new Dropbox API app with the Files and datastores setting and access only to files it creates (also known as App Folder permission).

3. Set App name to “Grocery Dude by Your Name,” or something else unique.

The Dropbox website layout is subject to change in the future, so you may need to refer to their own tutorials on creating a new Sync API app. Generally, however, the expected result is shown in Figure 13.2.

Image

Figure 13.2 Data backup with Dropbox

The Dropbox API app page should also show an app key and app secret. Write these both down because you’ll need to substitute them into code shortly. For demonstration purposes, APP_KEY and APP_SECRET will be used in the sample code, yet should be replaced by your own key and secret.


Note

To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter12.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name.


Supporting Frameworks

To add Dropbox support to Grocery Dude, the first thing you’ll need to do is download the Dropbox Sync API iOS SDK. At the time of writing the latest version is 1.1.3, and unfortunately it doesn’t yet support the new iOS 64-bit architecture. The downloadable sample project will be updated once a new supporting release is available. In addition to the SDK, Grocery Dude will need to link to a number of supporting frameworks.

Update Grocery Dude as follows to add the required frameworks to support Dropbox:

1. Download and extract the latest Sync API iOS SDK from the Dropbox website (http://www.dropbox.com/developers/sync). At the time of writing, v1.1.3 was located at Sync API > Install SDK > iOS.

2. Drag the downloaded Dropbox.framework directory into the Frameworks folder of Grocery Dude, ensuring that “Copy items into destination group’s folder” and the Grocery Dude target is ticked before clicking Finish.

3. Select the Grocery Dude target and scroll down to the Linked Frameworks and Libraries section of the General tab, as shown in Figure 13.3.

Image

Figure 13.3 Additional frameworks required to support Dropbox

4. Add the libc++.dylibCFNetworkSecuritySystemConfiguration, and QuartzCore frameworks, as shown in Figure 13.3.

Linking to Dropbox

Integrating Grocery Dude with Dropbox requires a new application:openURL method in the AppDelegate and a didFinishLaunchingWithOptions update. This boilerplate code was provided in the Dropbox Sync API for iOS tutorial on the Dropbox website. Listing 13.1 shows the code involved.

Listing 13.1 AppDelegate.m


#pragma mark - DROPBOX
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
  sourceApplication:(NSString *)source annotation:(id)annotation {

    DBAccount *account = [[DBAccountManager sharedManager] handleOpenURL:url];
    if (account) {
        DBFilesystem *filesystem = [[DBFilesystem alloc] initWithAccount:account];
        [DBFilesystem setSharedFilesystem:filesystem];
        NSLog(@"Linked to Dropbox!");
        return YES;
    }
    return NO;
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    DBAccountManager* accountMgr =
    [[DBAccountManager alloc] initWithAppKey:@"APP_KEY" secret:@"APP_SECRET"];
    [DBAccountManager setSharedManager:accountMgr];
    DBAccount *account = accountMgr.linkedAccount;
    if (account) {
        DBFilesystem *filesystem = [[DBFilesystem alloc] initWithAccount:account];
        [DBFilesystem setSharedFilesystem:filesystem];
    }
    return YES;
}


Update Grocery Dude as follows to enable Dropbox integration:

1. Add #import <Dropbox/Dropbox.h> to the top of AppDelegate.m.

2. Replace the existing didFinishLaunchingWithOptions method of AppDelegate.m with the code from Listing 13.1, including the new application:openURL method.

3. Update APP_KEY and APP_SECRET in didFinishLaunchingWithOptions with the key and secret you noted earlier.

The next step is to update Grocery Dude’s information property list to register for a URL scheme, which is required for authentication to work. The URL scheme will be your APP_KEY prefixed with db-, as shown in Listing 13.2.

Listing 13.2 Grocery Dude-Info.plist


<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>db-APP_KEY</string>
        </array>
    </dict>
</array>


Update Grocery Dude as follows to register for the URL scheme required for authentication:

1. Right-click Supporting Files/Grocery Dude-Info.plist and then select Open As > Source Code.

2. Paste the code from Listing 13.2 on the line after the first <dict> tag in Grocery Dude-Info.plist.

3. Replace APP_KEY in Grocery Dude-Info.plist with the key you noted earlier, ensuring to leave the db- prefix in place.

Introducing DropboxHelper

To make it easy for you to add Dropbox support to your own applications, most of the integration code will be put into a class called DropboxHelper. This class will have convenience methods used to perform the following:

image Link to and unlink from a Dropbox account

image Manage files between the Dropbox cache and local file system

image Back up and restore from ZIP

The starting point header for DropboxHelper is shown in Listing 13.3. It begins with two methods: one used to link and another used to unlink an account. The method that links an account to Dropbox requires that a view controller be given so the user authentication screen can originate from it.

Listing 13.3 DropboxHelper.h


#import <Foundation/Foundation.h>
#import <Dropbox/Dropbox.h>
#import "CoreDataHelper.h"

@interface DropboxHelper : NSObject

#pragma mark - DROPBOX ACCOUNT
+ (void)linkToDropboxWithUI:(UIViewController*)controller;
+ (void)unlinkFromDropbox;
@end


Update Grocery Dude as follows to add the DropboxHelper class:

1. Select the Grocery Dude group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to NSObject and Class name to DropboxHelper and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in DropboxHelper.h with the code from Listing 13.3.

You need to be aware of several new Dropbox classes that are available since linking to the Dropbox framework. A description of each class is available from the class reference included with the SDK you’ve already downloaded. Using Finder, right-click the com.dropbox.Dropbox.docset file and select Show Package Contents. Next, open the documentation from Contents/Resources/Documents/Index.html. You may recognize the classes from the new Dropbox code in AppDelegate.m.

The first two Dropbox classes to be used by DropboxHelper are DBAccount and DBAccountManager. The account manager is used to get reference to the linked account. The code to link and unlink an account is shown in Listing 13.4.

Listing 13.4 DropboxHelper.m


#import "DropboxHelper.h"
@implementation DropboxHelper

#pragma mark - DROPBOX ACCOUNT
+ (void)linkToDropboxWithUI:(UIViewController*)controller {
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (!account.isLinked) {
        NSLog(@"Linking to Dropbox...");
        [[DBAccountManager sharedManager] linkFromController:controller];
    } else {
        NSLog(@"Already linked to Dropbox as %@", account.info.displayName);
    }
}
+ (void)unlinkFromDropbox {
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (account.isLinked) {
        [account unlink];
        NSLog(@"Unlinked from Dropbox");
    }
}
@end


Update Grocery Dude as follows to link to Dropbox:

1. Replace all code in DropboxHelper.m with the code from Listing 13.4.

Introducing DropboxTVC

To create backups and perform restores, a new user interface is required. In addition, the user needs a way to link and unlink from Dropbox in case he or she needs to switch accounts. To support the new interface, a new tab called Backups will be added. This tab will contain a table view used to display a list of backups. This table view will be driven by a new UITableViewController subclass called DropboxTVC. This class will first be used to demonstrate linking to Dropbox and will then be expanded to provide the backup-and-restore capability.

The starting point header for DropboxTVC is shown in Listing 13.5. It begins with two properties, which will be set as the view loads. The contents property will store an array of DBFileInfo objects representing the contents of the Dropbox cache. This array will drive what’s displayed in the table view. Another property called loading will be used to indicate that the table view contents are in the process of loading, to prevent an unnecessary reload of contents.

Listing 13.5 DropboxTVC.h


#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "CoreDataHelper.h"
#import "DropboxHelper.h"

@interface DropboxTVC : UITableViewController
@property (strong, nonatomic) NSMutableArray *contents;
@property (assign, nonatomic) BOOL loading;
@end


Update Grocery Dude as follows to add the DropboxTVC class:

1. Select the Grocery Dude Table View Controllers group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to UITableViewController and Class name to DropboxTVC and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in DropboxTVC.h with the code from Listing 13.5.

To demonstrate linking to Dropbox, the DropboxTVC implementation will begin with just one method. The viewDidLoad method will set each of the previously mentioned properties and proceed to link with Dropbox. Listing 13.6 shows the code involved.

Listing 13.6 DropboxTVC.m


#import "DropboxTVC.h"
@implementation DropboxTVC

#pragma mark - VIEW
- (void)viewDidLoad {
    _contents = [NSMutableArray new];
    _loading = NO;
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (!account.isLinked) {
        [DropboxHelper linkToDropboxWithUI:self];
    }
    [super viewDidLoad];
}
@end


Update Grocery Dude as follows to implement DropboxTVC:

1. Replace all code in DropboxTVC.m with the code from Listing 13.6.

The next step is to add the interface elements that will leverage the DropboxTVC class. This involves configuring a new Backups tab and DropboxTVC table view controller. The Backups tab will need an icon, as will the backup files that will be listed in the Dropbox table view.

Update Grocery Dude as follows to configure the Dropbox tab and table view:

1. Select Images.xcassets.

2. Download and extract the images from the following URL and then drag them to the inside of the Images.xcassets asset catalog: http://www.timroadley.com/LearningCoreData/Icons_DataBackup.zip.

3. Select Main.storyboard.

4. Drag a new Table View Controller onto the storyboard and then align it beneath the existing Navigation Controller – Shop.

5. Ensure the new Table View Controller is selected and click Editor > Embed In > Navigation Controller.

6. Hold down Control and drag a line from the center of the Tab Bar Controller to the new Navigation Controller and then select Relationship Segue > view controllers from the popup menu.

7. Set the Bar Item Title of the new Navigation Controller to Backups using Attributes Inspector (Option+image+4).

8. Set the Bar Item Image of the new Navigation Controller to data. The expected result is shown in Figure 13.4.

Image

Figure 13.4 The new Backups tab

9. Set the Navigation Item Title of the new Table View Controller to Backups.

10. Set the Identifier of the Backups Table View Controller Prototype Cell to Backup Cell using Attributes Inspector (Option+image+4).

11. Set the Custom Class of the Backups Table View Controller to DropboxTVC using Identity Inspector (Option+image+3). Take care not to accidentally set the custom class of the UITableViewCell.

12. Delete the application from your device or the iOS Simulator so the persistent store has default data; then click Product > Clean in Xcode.

13. Run the application and select the Backups tab. You should be presented with a Dropbox authentication view such as the one shown in Figure 13.5.

Image

Figure 13.5 Dropbox link

Once you’ve entered your Dropbox account credentials, the text “Linked to Dropbox!” should appear in the console log. If you get an error in the console log regarding the URL scheme, ensure the application key and secret have been set correctly.

Preparing CoreDataHelper

Before DropboxHelper can be further developed to facilitate backup and restore, CoreDataHelper needs to be enhanced to support reloading the store. When a restore occurs, the entire store’s path will be replaced. When this happens, all contexts should be cleared and the old store files and underlying path removed. The Core Data stack should then be set up again as per usual with setupCoreData. New methods will now be added to CoreDataHelper to assist with this. The code involved is shown in Listing 13.7.

Listing 13.7 CoreDataHelper.m: CORE DATA RESET


#pragma mark - CORE DATA RESET
- (void)resetContext:(NSManagedObjectContext*)moc {
    [moc performBlockAndWait:^{
        [moc reset];
    }];
}
- (BOOL)reloadStore {
    BOOL success = NO;
    NSError *error = nil;
    if (![_coordinator removePersistentStore:_store error:&error]) {
        NSLog(@"Unable to remove persistent store : %@", error);
    }
    [self resetContext:_sourceContext];
    [self resetContext:_importContext];
    [self resetContext:_context];
    [self resetContext:_parentContext];
    _store = nil;
    [self setupCoreData];
    [self somethingChanged];
    if (_store) {success = YES;}
    return success;
}


Update Grocery Dude as follows to prepare CoreDataHelper:

1. Add the code from Listing 13.7 to the bottom of CoreDataHelper.m before @end.

2. Add - (BOOL)reloadStore; to the bottom of CoreDataHelper.h before @end.

Building DropboxHelper

Most of the legwork in creating backups and performing restores will involve file management. In particular, support for creating ZIP files and moving them between the local file system and Dropbox will be required. DropboxHelper will provide as many of these convenience methods as possible so the functionality can be ported to other applications easily. DropboxHelper will need three new sections to support the backup-and-restore capability.

Local File Management

The LOCAL FILE MANAGEMENT section will consist of three methods used during the backup-and-restore processes:

image renameLastPathComponentOfURL will be used to rename files at the given URL. In reality, a move will be executed to achieve the rename.

image deleteFileAtURL will delete the file at the given URL, provided the file exists.

image createParentFolderForFile will be used to ensure that the ZIP file extraction target folder exists before extraction occurs.

Listing 13.8 shows the code involved with this new section.

Listing 13.8 DropboxHelper.m: LOCAL FILE MANAGEMENT


#pragma mark - LOCAL FILE MANAGEMENT
+ (NSURL*)renameLastPathComponentOfURL:(NSURL*)url toName:(NSString*)name {

    NSURL *urlPath = [url URLByDeletingLastPathComponent];
    NSURL *newURL = [urlPath URLByAppendingPathComponent:name];
    NSError *error;
    [[NSFileManager defaultManager] moveItemAtPath:url.path
                                            toPath:newURL.path error:&error];
    if (error) {
        NSLog(@"ERROR renaming (i.e. moving) %@ to %@",
          url.lastPathComponent, newURL.lastPathComponent);
    } else {
        NSLog(@"Renamed %@ to %@", url.lastPathComponent, newURL.lastPathComponent);
    }
    return newURL;
}
+ (BOOL)deleteFileAtURL:(NSURL*)url {

    if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
        NSError *error;
        [[NSFileManager defaultManager] removeItemAtPath:url.path error:&error];
        if (error) {NSLog(@"Error deleting %@", url.lastPathComponent);}
        else {NSLog(@"Deleted '%@'", url.lastPathComponent);return YES;}
    }
    return NO;
}
+ (void)createParentFolderForFile:(NSURL*)url {

    NSURL *parent = [url URLByDeletingLastPathComponent];
    if (![[NSFileManager defaultManager] fileExistsAtPath:parent.path]) {
        NSError *error;
        [[NSFileManager defaultManager] createDirectoryAtURL:parent
                                 withIntermediateDirectories:YES
                                                  attributes:nil
                                                       error:&error];
        if (error) {NSLog(@"Error creating directory: %@", error);}
    }
}


Update Grocery Dude as follows to implement this new section:

1. Copy the code from Listing 13.8 to the bottom of DropboxHelper.m before @end.

Dropbox File Management

The DROPBOX FILE MANAGEMENT section will consist of five methods used to manage files within the Dropbox cache:

image fileExistsAtDropboxPath will be used to check if a file exists in the Dropbox cache at the specified path.

image listFilesAtDropboxPath will be used to list the contents of the given Dropbox path in the console log. It runs on a background thread without fear of threading issues as the only method output is console logging.

image deleteFileAtDropboxPath will be used to delete a file at the specified Dropbox path.

image copyFileAtDropboxPath:toURL will be used to copy files from Dropbox to the local file system.

image copyFileAtURL:toDropboxPath will be used to copy files from the local file system to the Dropbox cache.

Listing 13.9 shows the code involved with this new section.

Listing 13.9 DropboxHelper.m: DROPBOX FILE MANAGEMENT


#pragma mark - DROPBOX FILE MANAGEMENT
+ (BOOL)fileExistsAtDropboxPath:(DBPath*)dropboxPath {

    DBFile *existingFile =
    [[DBFilesystem sharedFilesystem] openFile:dropboxPath error:nil];
    if (existingFile) {
        [existingFile close];
        return YES;
    }
    return NO;
}
+ (void)listFilesAtDropboxPath:(DBPath*)dropboxPath {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSError *error = nil;
        NSArray *contents =
        [[DBFilesystem sharedFilesystem] listFolder:dropboxPath error:&error];
        if (contents) {
            NSLog(@"****** Dropbox Directory Contents (path /%@)",
                                                           dropboxPath.stringValue);
            for (DBFileInfo *info in contents) {
                float fileSize = info.size;
                NSLog(@" %@ (%.2fMB)", info.path, fileSize/1024/1024);
            }
            NSLog(@"******************************************");
            if (error) {
                NSLog(@"ERROR listing Dropbox contents for %@ : %@",
                                                    dropboxPath.stringValue, error);
            }
        } else {
            NSLog(@"Dropbox path '/%@' is empty", dropboxPath.stringValue);
        }
    });
}
+ (void)deleteFileAtDropboxPath:(DBPath*)dropboxPath {
    [[DBFilesystem sharedFilesystem] deletePath:dropboxPath error:nil];
}
+ (void)copyFileAtDropboxPath:(DBPath*)dropboxPath toURL:(NSURL*)url {

    DBError *openError = nil;
    DBFile *file = [[DBFilesystem sharedFilesystem] openFile:dropboxPath
                                                       error:&openError];
    if (openError) {
        NSLog(@"Error opening file '%@': %@", dropboxPath.stringValue, openError);
    }
    DBError *readError = nil;
    NSData *fileData = [file readData:&readError];
    if (readError) {
        NSLog(@"Error reading file '%@': %@", dropboxPath.stringValue, readError);
}
    [self deleteFileAtURL:url];
    [[NSFileManager defaultManager] createFileAtPath:url.path
                                            contents:fileData
                                          attributes:nil];
}
+ (void)copyFileAtURL:(NSURL*)url toDropboxPath:(DBPath*)dropboxPath {

    NSLog(@"Copying %@ to Dropbox Path %@", url, dropboxPath);

    // Create File
    DBError *errorCreating;
    DBFile *file =
    [[DBFilesystem sharedFilesystem] createFile:dropboxPath error:&errorCreating];
    if (!file || errorCreating) {
        NSLog(@"Error creating file in Dropbox: %@", errorCreating);
    }

    // Write File
    DBError *errorWriting;
    if ([file writeContentsOfFile:url.path shouldSteal:NO error:&errorWriting]) {
        NSLog(@"Successfully copied %@ to Dropbox:%@",
                                   url.lastPathComponent, dropboxPath.stringValue);
    } else {
        NSLog(@"Error writing file to Dropbox: %@", errorWriting);
    }
}


Update Grocery Dude as follows to implement this new section:

1. Copy the code vfrom Listing 13.9 to the bottom of DropboxHelper.m before @end.

Backup & Restore

The BACKUP & RESTORE section introduces three new methods used to orchestrate a backup or restore process. These methods will leverage those recently implemented.

image zipFolderAtURL will be used to create a ZIP file containing the contents of a given URL. If the ZIP file already exists, it will be overwritten. The ZIP compression will be provided by Objective-Zip, which is an Objective-C wrapper for MiniZip. MiniZip enables the creation and extraction of compressed .zip archive files.

image unzipFileAtURL will be used to extract the contents of a given ZIP file to the given URL. Objective-Zip will be used for decompression.

image restoreFromDropboxStoresZip will be used to restore the Stores folder from the contents of the given ZIP file. Measures will be taken to enable rollback in case the restore fails to leave the Core Data stack in a working state. This method will rely on knowledge of where the application stores directory is located. As such, the applicationStoresDirectory method of CoreDataHelper will have to be made public.

Listing 13.10 shows the code involved with this new section.

Listing 13.10 DropboxHelper.m: BACKUP / RESTORE


#pragma mark - BACKUP / RESTORE
+ (NSURL*)zipFolderAtURL:(NSURL*)url withZipfileName:(NSString*)zipFileName {

    NSURL *zipFileURL =
    [[url URLByDeletingLastPathComponent] URLByAppendingPathComponent:zipFileName];

    // Remove existing zip
    [self deleteFileAtURL:zipFileURL];

    // Create new zip
    ZipFile *zipFile =
    [[ZipFile alloc] initWithFileName:zipFileURL.path mode:ZipFileModeCreate];

    // Enumerate directory structure
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSDirectoryEnumerator *directoryEnumerator =
    [fileManager enumeratorAtPath:url.path];

    // Write zip files for each file in the directory structure
    NSString *fileName;
    while (fileName = [directoryEnumerator nextObject]) {
        BOOL directory;
        NSString *filePath = [url.path stringByAppendingPathComponent:fileName];
        [fileManager fileExistsAtPath:filePath isDirectory:&directory];
        if (!directory) {

            // get file attributes
            NSError *error = nil;
            NSDictionary *attributes =
            [[NSFileManager defaultManager]attributesOfItemAtPath:filePath
                                                            error:&error];
            if (error) {
             NSLog(@"Failed to create zip, could not get file attributes. Error: %@",
                                                                              error);
                return nil;
            } else {
                NSDate *fileDate = [attributes objectForKey:NSFileCreationDate];
                ZipWriteStream *stream =
                [zipFile writeFileInZipWithName:fileName
                                       fileDate:fileDate
                               compressionLevel:ZipCompressionLevelBest];
                NSData *data = [NSData dataWithContentsOfFile:filePath];
                [stream writeData:data];
                [stream finishedWriting];
            }
        }
    }
    [zipFile close];

    return zipFileURL;
}
+ (void)unzipFileAtURL:(NSURL*)zipFileURL toURL:(NSURL*)unzipURL {

     @autoreleasepool {
         ZipFile *unzipFile = [[ZipFile alloc] initWithFileName:zipFileURL.path
                                                           mode:ZipFileModeUnzip];
         [unzipFile goToFirstFileInZip];
         for (int i = 0; i < [unzipFile numFilesInZip]; i++) {
            FileInZipInfo *info = [unzipFile getCurrentFileInZipInfo];
            [self createParentFolderForFile:
                                  [unzipURL URLByAppendingPathComponent:info.name]];
            NSLog(@"Unzipping '%@'...", info.name);
            ZipReadStream *read = [unzipFile readCurrentFileInZip];
            NSMutableData *data = [[NSMutableData alloc] initWithLength:info.length];
            [read readDataWithBuffer:data];
            [data writeToFile:[NSString stringWithFormat:@"%@/%@",
                                           unzipURL.path, info.name] atomically:YES];
            [read finishedReading];
            [unzipFile goToNextFileInZip];
         }
         [unzipFile close];
     }
}
+ (void)restoreFromDropboxStoresZip:(NSString*)fileName
                 withCoreDataHelper:(CoreDataHelper*)cdh {

    [cdh.context performBlock:^{

        DBPath *zipFileInDropbox = [[DBPath alloc] initWithString:fileName];
        NSURL  *zipFileInSandbox =
        [[[cdh applicationStoresDirectory] URLByDeletingLastPathComponent]
                                          URLByAppendingPathComponent:fileName];
        NSURL  *unzipFolder =
        [[[cdh applicationStoresDirectory] URLByDeletingLastPathComponent]
                                          URLByAppendingPathComponent:@"Stores_New"];
        NSURL *oldBackupURL =
        [[[cdh applicationStoresDirectory] URLByDeletingLastPathComponent]
                                          URLByAppendingPathComponent:@"Stores_Old"];

        [DropboxHelper copyFileAtDropboxPath:zipFileInDropbox
                                       toURL:zipFileInSandbox];
        [DropboxHelper unzipFileAtURL:zipFileInSandbox toURL:unzipFolder];

        if ([[NSFileManager defaultManager] fileExistsAtPath:unzipFolder.path]) {
            [DropboxHelper deleteFileAtURL:oldBackupURL];
         [DropboxHelper renameLastPathComponentOfURL:[cdh applicationStoresDirectory]
                                              toName:@"Stores_Old"];
         [DropboxHelper renameLastPathComponentOfURL:unzipFolder
                                              toName:@"Stores"];
            if ([cdh reloadStore]) {
                [DropboxHelper deleteFileAtURL:oldBackupURL];
                UIAlertView *failAlert = [[UIAlertView alloc]
           initWithTitle:@"Restore Complete!"
                 message:@"All data has been restored from the selected backup"
                delegate:nil
       cancelButtonTitle:nil
       otherButtonTitles:@"Ok", nil];
                [failAlert show];

            } else { // Attempt Recovery
         [DropboxHelper renameLastPathComponentOfURL:[cdh applicationStoresDirectory]
                                              toName:@"Stores_FailedRestore"];
         [DropboxHelper renameLastPathComponentOfURL:oldBackupURL
                                              toName:@"Stores"];
                [DropboxHelper deleteFileAtURL:oldBackupURL];
                if (![cdh reloadStore]) {
                    UIAlertView *failAlert = [[UIAlertView alloc]
                          initWithTitle:@"Failed to Restore"
                                message:@"Please try to restore from another backup"
                               delegate:nil
                      cancelButtonTitle:nil
                      otherButtonTitles:@"Close", nil];
                    [failAlert show];
                }
            }
        }
    }];
}


Update Grocery Dude as follows to implement this new section:

1. Download, extract, and drag the contents of the following ZIP file into the Grocery Dude group: www.timroadley.com/LearningCoreData/Objective-Zip.zip. Ensure that “Copy items into destination group’s folder” and the Grocery Dude target is ticked before clicking Finish. This ZIP file contains the most recent version of Objective-Zip at the time of writing, and a header that imports each Objective-Zip class.

2. Add #import "Objective-Zip.h" to the top of DropboxHelper.m.

3. Add the following code to the bottom of CoreDataHelper.h before @end:

- (NSURL *)applicationStoresDirectory;

4. Copy the code from Listing 13.10 to the bottom of DropboxHelper.m before @end.

So that the methods of DropboxHelper may be accessed outside the class, they need to be added to its header file. Listing 13.11 shows the code involved.

Listing 13.11 DropboxHelper.h


#pragma mark - LOCAL FILE MANAGEMENT
+ (NSURL*)renameLastPathComponentOfURL:(NSURL*)url toName:(NSString*)name;
+ (BOOL)deleteFileAtURL:(NSURL*)url;
+ (void)createParentFolderForFile:(NSURL*)url;

#pragma mark - DROPBOX FILE MANAGEMENT
+ (BOOL)fileExistsAtDropboxPath:(DBPath*)dropboxPath;
+ (void)listFilesAtDropboxPath:(DBPath*)dropboxPath;
+ (void)deleteFileAtDropboxPath:(DBPath*)dropboxPath;
+ (void)copyFileAtDropboxPath:(DBPath*)dropboxPath toURL:(NSURL*)url;
+ (void)copyFileAtURL:(NSURL*)url toDropboxPath:(DBPath*)dropboxPath;

#pragma mark - BACKUP / RESTORE
+ (NSURL*)zipFolderAtURL:(NSURL*)url withZipfileName:(NSString*)zipFileName;
+ (void)unzipFileAtURL:(NSURL*)zipFileURL toURL:(NSURL*)unzipURL;
+ (void)restoreFromDropboxStoresZip:(NSString*)fileName
                 withCoreDataHelper:(CoreDataHelper*)cdh;


Update Grocery Dude as follows to make the new methods visible:

1. Add the code from Listing 13.11 to the bottom of DropboxHelper.h before @end.

Building DropboxTVC

With DropboxHelper in place, it’s now time to use it to create, display, and restore backups. The DropboxTVC class will now be expanded to leverage its functionality. Three new properties and two protocol adoptions will be added to the DropboxTVC header:

image To link or unlink from Dropbox, an options action-sheet will be used. DropboxTVC will also become a UIActionSheetDelegate to receive user selections.

image To confirm that a user wants to restore, a confirmRestore alert view property will be added. DropboxTVC will also become a UIAlertViewDelegate to receive user selections from the alert view.

image To store the name of the ZIP file selected for restore, a string property called selectedZipFileName will be used.

Listing 13.12 shows the three new properties.

Listing 13.12 DropboxTVC.h


@property (strong, nonatomic) UIActionSheet *options;
@property (strong, nonatomic) UIAlertView *confirmRestore;
@property (strong, nonatomic) NSString *selectedZipFileName;


Update Grocery Dude as follows to add the three new properties and adopt the relevant protocols:

1. Add the properties from Listing 13.12 to the bottom of DropboxTVC.h before @end.

2. Configure DropboxTVC.h to adopt the alert view and action sheet protocols by replacing the @interface declaration with the following code:

@interface DropboxTVC : UITableViewController
<UIAlertViewDelegate, UIActionSheetDelegate>

The contents of the Backups table view driven by DropboxTVC will be determined by the contents of the local Dropbox cache. A contents array will be used to populate the table view and will need to be updated as the Dropbox cache changes. A new method called reload will be used to populate the contents array with the Dropbox cache listing. A new function called sort will be used to sort the array by modified date. As files are added and removed, the synchronization status will be shown in the navigation item title. A new method called refreshStatus will be used to update this information. The status is taken from the value of the status property of the shared DBFilesystem. Note that there will be no support for folder navigation. As such, folders will be removed from the contents array. Listing 13.13 shows the code involved.

Listing 13.13 DropboxTVC.m: DATA


#pragma mark - DATA
NSInteger sort(DBFileInfo *a, DBFileInfo *b, void *ctx) {
    return [[b modifiedTime] compare:[a modifiedTime]];
}
- (void)reload {
    [self refreshStatus];
    if (_loading) return;_loading = YES;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^() {
        NSArray *actualContents =
        [[DBFilesystem sharedFilesystem] listFolder:[DBPath root] error:nil];
        NSMutableArray *updatedContents =
        [NSMutableArray arrayWithArray:actualContents];

        // Don't list folders
        NSMutableArray *folders = [NSMutableArray new];
        for (DBFileInfo *info in updatedContents) {
            if (info.isFolder) {[folders addObject:info];}
        }
        [updatedContents removeObjectsInArray:folders];

        // Don't list files that don't end with 'Stores.zip'
        NSMutableArray *notValid = [NSMutableArray new];
        for (DBFileInfo *info in updatedContents) {
            if (![[[info path] stringValue] hasSuffix:@"Stores.zip"]) {
                NSLog(@"Not listing invalid file: %@", [[info path] stringValue]);
                [notValid addObject:info];
            }
        }
        [updatedContents removeObjectsInArray:notValid];

        [updatedContents sortUsingFunction:sort context:NULL];
        dispatch_async(dispatch_get_main_queue(), ^() {
            self.contents = updatedContents;
            _loading = NO;
            [self.tableView reloadData];
            [self refreshStatus];
        });
    });
}
- (void)refreshStatus {
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (!account.isLinked) {
        self.navigationItem.title = @"Unlinked";
    } else if ([[DBFilesystem sharedFilesystem] status] > DBSyncStatusActive) {
        self.navigationItem.title = @"Syncing";
    } else {
        self.navigationItem.title = @"Backups";
    }
}


Update Grocery Dude as follows to implement contents reloading code:

1. Add the code from Listing 13.13 to DropboxTVC.m just above the existing VIEW section.

The next step is to leverage the reload method at the appropriate times. Each time the view appears, the table should be reloaded to match an appropriate representation of the underlying Dropbox contents. In addition, an observer is needed to trigger a reload in case the Dropbox cache changes while the view is visible. To keep the sync status visible to the user, the sync status will be observed and refreshed whenever a change is detected, too. Listing 13.14 shows the code involved.

Listing 13.14 DropboxTVC.m: VIEW


- (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
    __block DropboxTVC *DropboxTVC = self;
    [[DBFilesystem sharedFilesystem] addObserver:self block:^(){[self reload];}];
    [[DBFilesystem sharedFilesystem] addObserver:self
                              forPathAndChildren:[DBPath root] block:^() {
        [DropboxTVC reload];
    }];
    [DropboxTVC reload];
}
- (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];
        [[DBFilesystem sharedFilesystem] removeObserver:self];
}


Update Grocery Dude as follows to implement the code to trigger timely reloads:

1. Add the code from Listing 13.14 to the bottom of the existing VIEW section of DropboxTVC.m.

Creating Backups

Creating a backup is easy now that DropboxHelper is in place. Before a backup is taken, all contexts should be saved to ensure that the latest data is persisted. The contexts will be saved in an order appropriate to their hierarchy. In other words, the child contexts will be saved before their parents to ensure changes are in the parent before the parent is saved. In your own projects you may wish to introduce non-nil error handling for these saves as opposed to the approach used in the upcoming code.

The procedure to create a backup involves setting an appropriate backup ZIP filename, creating the backup ZIP, and then moving it to Dropbox. There will also be a small amount of logic to deal with existing files. The process will only work if there is an account linked to Dropbox. Depending on the result of the backup, an appropriate alert view will be shown. Listing 13.15 shows the code involved.

Listing 13.15 DropboxTVC.m: BACKUP


#pragma mark - BACKUP
- (IBAction)backup:(id)sender {
    [DropboxHelper linkToDropboxWithUI:self];
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (account.isLinked) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        [cdh.context performBlock:^{
            // Save all contexts
            [cdh.sourceContext performBlockAndWait:^{[cdh.sourceContext save:nil];}];
            [cdh.importContext performBlockAndWait:^{[cdh.importContext save:nil];}];
            [cdh.context performBlockAndWait:^{[cdh.context save:nil];}];
            [cdh.parentContext performBlockAndWait:^{[cdh.parentContext save:nil];}];

            NSLog(@"Creating a dated backup of the Stores directory...");
            NSDateFormatter *formatter = [NSDateFormatter new];
            [formatter setDateFormat:@"[yyyy-MMM-dd] hh.mm a"];
            NSString *date = [formatter stringFromDate:[NSDate date]];
            NSString *zipFileName =
            [NSString stringWithFormat:@"%@ Stores.zip", date];
            NSURL *zipFile =
            [DropboxHelper zipFolderAtURL:[cdh applicationStoresDirectory]
                          withZipfileName:zipFileName];

            NSLog(@"Copying the backup zip to Dropbox...");
            DBPath *zipFileInDropbox =
            [[DBPath root] childPath:zipFile.lastPathComponent];
            if ([DropboxHelper fileExistsAtDropboxPath:zipFileInDropbox]) {
                NSLog(@"Removing existing backup with same name...");
                [DropboxHelper deleteFileAtDropboxPath:zipFileInDropbox];
            }
            [DropboxHelper copyFileAtURL:zipFile toDropboxPath:zipFileInDropbox];
            NSLog(@"Deleting the local backup zip...");
            [DropboxHelper deleteFileAtURL:zipFile];
            [DropboxHelper listFilesAtDropboxPath:[DBPath root]];
            [self alertSuccess:YES];
        }];
    } else {
        [self alertSuccess:NO];
    }
}
- (void)alertSuccess:(BOOL)success {
    NSString *title;
    NSString *message;
    if (success) {
        title = [NSString stringWithFormat:@"Success"];
        message = [NSString stringWithFormat:@"A backup has been created. It will appear in the Apps/Grocery Dude directory of your Dropbox. Consider removing old backups when you no longer require them"];
    } else {
        title = [NSString stringWithFormat:@"Fail"];
        message = @"You must be logged in to Dropbox to create backups";
    }
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
                                                    message:message
                                                   delegate:nil
                                          cancelButtonTitle:nil
                                          otherButtonTitles:@"Ok", nil];
    [alert show];
}


Update Grocery Dude as follows to implement the code to back up and link it to a new button:

1. Add the code from Listing 13.15 to the bottom of DropboxTVC.m before @end.

2. Select Main.storyboard.

3. Drag a Bar Button Item to the top left of the Backups Table View Controller.

4. Set the Bar Item Title of the new Bar Button Item to Create using Attributes Inspector (Option+image+4).

5. Set the Bar Item Style of the new Bar Button Item to Done.

6. Hold Control and drag a line from the Create button to the yellow circle at the bottom of the Backups Table View Controller; then select Sent Actions > backup:.

Run the application and create a backup by tapping Create on the Backups tab. The console log may warn you that you already have a linked account, and that thedate_stores.zip doesn’t exist. These messages are normal checks performed as the account is verified and the backup file is created. Figure 13.6 shows the expected result: The table view will remain empty, as it hasn’t yet been configured to display anything.

Image

Figure 13.6 Successful backup

Displaying Backups

To display a list of available backups, the DropboxTVC needs to be updated with the appropriate UITableView data source methods. There will only be one section, and the number of rows will depend on the number of objects in the contents array. The table view cell will be configured to show a user-friendly version of the backup filename, without the stores or ZIP file extension. Invalid files, identified as not ending with Stores.zip, won’t be displayed. For the remaining files, the file size and upload or download progress will be shown where relevant. For convenience, swipe-to-delete will also be added. Listing 13.16 shows the code involved.

Listing 13.16 DropboxTVC.m: DATASOURCE: UITableView


#pragma mark - DATASOURCE: UITableView
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section {
    return [_contents count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Backup Cell";
    UITableViewCell *cell =
    [tableView dequeueReusableCellWithIdentifier:CellIdentifier
                                    forIndexPath:indexPath];
    DBFileInfo *info = [_contents objectAtIndex:[indexPath row]];
    NSString *string = info.path.name;
    cell.textLabel.text =
    [string stringByReplacingOccurrencesOfString:@" Stores.zip" withString:@""];
    float fileSize = info.size;

    NSMutableString *subtitle = [NSMutableString new];

    // Show transfer progress
    DBError *openError = nil;
    DBFile *file = [[DBFilesystem sharedFilesystem] openFile:info.path
                                                       error:&openError];
    if (!file) {
        NSLog(@"Error opening file '%@': %@", info.path.stringValue, openError);
    }
    int progress = [[file status] progress] * 100;
    if (progress != 100) {
        if ([[file status] state] == DBFileStateDownloading) {
            [subtitle appendString:
                    [NSString stringWithFormat:@"Downloaded %i%% of ",progress]];
        } else if ([[file status] state] == DBFileStateUploading) {
            [subtitle appendString:
                    [NSString stringWithFormat:@"Uploaded %i%% of ",progress]];
        }
    }

    // Show File Size
    [subtitle appendString:
               [NSString stringWithFormat:@"%.2f Megabytes", fileSize/1024/1024]];
    cell.detailTextLabel.text = subtitle;

    return cell;
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
 forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (tableView == self.tableView &&
        editingStyle == UITableViewCellEditingStyleDelete) {
        DBFileInfo *deleteTarget = [_contents objectAtIndex:indexPath.row];
        [DropboxHelper deleteFileAtDropboxPath:deleteTarget.path];
        [_contents removeObjectAtIndex:indexPath.row];

        [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                              withRowAnimation:UITableViewRowAnimationFade];
    }
}


Update Grocery Dude as follows to make backups visible to the user:

1. Add the code from Listing 13.16 to the bottom of DropboxTVC.m before @end.

2. Select Main.storyboard.

3. Set the Style of the Backups Table View Prototype Cell to Subtitle using Attributes Inspector (Option+image+4).

4. Set the Image of the Backups Table View Prototype Cell to backup.

Run Grocery Dude again, and you should see a backup listed on the Backups table view. Note that the population of this table view is asynchronous, so it may take a little while to update, particularly if this is the first time you’ve authenticated to Dropbox. Figure 13.7 shows the expected result.

Image

Figure 13.7 Backups

Restore

The restore process will require four new methods in DropboxTVC. Due to the destructive nature of a restore, the process will require the user to tap several buttons in order to initiate a restore. First, the user will need to select a file to restore. Second, the user will need to tap an options button and then tap restore. Finally, the user will need to confirm the restore action. This is a safeguard against an accidental restore. The new DropboxTVC methods are as follows:

image restore will be used to set the selected ZIP filename and show the restore confirmation alert view. If a file is still downloading, isn’t cached locally, or has a “newer” status, the restore will be skipped and the user notified.

image options will be tied to a new button that displays an action sheet allowing the user to choose to link/unlink from Dropbox, or to restore.

image actionSheet:clickedButtonAtIndex will be used to handle either linking/unlinking from Dropbox or to initiate a restore.

image alertView:clickedButtonAtIndex will be used to begin the restore process as long as the user has confirmed he or she would like this to occur.

The code involved is shown in Listing 13.17.

Listing 13.17 DropboxTVC.m: RESTORE


#pragma mark - RESTORE
- (void)restore {
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (!account.isLinked) {[DropboxHelper linkToDropboxWithUI:self];}
    if (account) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        if (indexPath) {
            DBFileInfo *info = [_contents objectAtIndex:indexPath.row];

            // Don't restore partially downloaded files
            if (![[[[DBFilesystem sharedFilesystem] openFile:info.path
                                                       error:nil] status] cached] ||
                  [[[DBFilesystem sharedFilesystem] openFile:info.path
                                                       error:nil] newerStatus]

                ) {

                UIAlertView *failAlert = [[UIAlertView alloc]
                                           initWithTitle:@"Failed to Restore"
                                                 message:@"The file is not ready"
                                                delegate:nil
                                       cancelButtonTitle:nil
                                       otherButtonTitles:@"Close", nil];
                [failAlert show];
                return;
            }
            _selectedZipFileName = info.path.name;
            NSLog(@"Selected '%@' for restore", _selectedZipFileName);
            NSString *restorePoint =
            [_selectedZipFileName stringByReplacingOccurrencesOfString:@" Stores.zip"
                                                            withString:@""];

            NSString *message = [NSString stringWithFormat:@"Are you sure want to restore from %@ backup? Existing data will be lost. The application may pause for the duration of the restore.", restorePoint];

            _confirmRestore = [[UIAlertView alloc] initWithTitle:nil
                                                         message:message
                                                        delegate:self
                                               cancelButtonTitle:@"Cancel"
                                               otherButtonTitles:@"Restore", nil];
            [_confirmRestore show];
        } else {
            UIAlertView *alert =
             [[UIAlertView alloc] initWithTitle:nil
                                        message:@"Please select a backup to restore"
                                       delegate:self
                              cancelButtonTitle:@"Ok"
                              otherButtonTitles:nil];
            [alert show];
        }
    }
}
- (IBAction)options:(id)sender {
    NSString *title, *toggleLink, *restore;
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (account.isLinked) {
        restore = [NSString stringWithFormat:@"Restore Selected Backup"];
        toggleLink = [NSString stringWithFormat:@"Unlink from Dropbox"];
        if (account.info.displayName) {
            title = [NSString stringWithFormat:@"Dropbox: %@",
                                                        account.info.displayName];
        } else {
            title = [NSString stringWithFormat:@"Dropbox: Linked"];
        }
    } else {
        toggleLink = [NSString stringWithFormat:@"Link to Dropbox"];
        title = [NSString stringWithFormat:@"Dropbox: Not Linked"];
    }
    _options = [[UIActionSheet alloc] initWithTitle:title
                                           delegate:self
                                  cancelButtonTitle:@"Cancel"
                             destructiveButtonTitle:nil
                                  otherButtonTitles:toggleLink,restore, nil];
    [_options showFromTabBar:self.navigationController.tabBarController.tabBar];
}
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex {
    DBAccount *account = [[DBAccountManager sharedManager] linkedAccount];
    if (actionSheet == _options) {
        switch (buttonIndex) {
            case 0:
                if (account.isLinked) {
                    [DropboxHelper unlinkFromDropbox];
                    [self reload];
                } else {
                    [DropboxHelper linkToDropboxWithUI:self];
                    [self reload];
                }
                break;
            case 1:
                [self restore];
                break;
            default:
                break;
        }
    }
}
- (void)alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (alertView == _confirmRestore && buttonIndex == 1) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        [DropboxHelper restoreFromDropboxStoresZip:_selectedZipFileName
                                withCoreDataHelper:cdh];
    }
}


Update Grocery Dude as follows to implement a restore:

1. Add the code from Listing 13.17 to the bottom of DropboxTVC.m before @end.

2. Select Main.storyboard.

3. Drag a Bar Button Item to the top right of the Backups Table View Controller.

4. Set the Bar Item Title of the new Bar Button Item to Options.

5. Hold down Control and drag a line from the Options button to the yellow circle at the bottom of the Backups Table View Controller; then select Sent Actions > options:. If options: is not an option on the popup menu, you may need to save DropboxTVC.m and try again.

Run Grocery Dude again, and you should be able to restore from a backup. Remember to first select a backup and then tap Options > Restore Selected Backup > Restore. Figure 13.8 shows the expected result.

Image

Figure 13.8 Restore

Note that the backup-and-restore processes run on the main thread, which will freeze the application for the duration of each. Unless the store is big, this will only be for a few seconds and the user is warned ahead of time. In reality, you should block user interaction the same way theMigrationVC view does in Chapter 3, “Managed Object Model Migration.” Using that approach, these processes could be performed in the background while an activity or progress indicator is displayed. These steps have been omitted from this chapter for brevity.

Summary

You’ve now been shown how to integrate an application with Dropbox for the purpose of backup and restore. The process of creating a backup ZIP file has also been demonstrated as the entire Stores directory was preserved within a ZIP file. Having a backup-and-restore option available to your users can make them feel that their data is safer. This should make them more comfortable with storing important data on their device. Even if their device were stolen, they would not lose any data provided they had created a backup. The backup-and-restore option is a great (if rudimentary) option for transferring data between devices with the same application.

Exercises

Why not build on what you’ve learned by experimenting?

1. Try unlinking and relinking to Dropbox using the Options menu on the Data tab. Note that this will clear the local Dropbox cache so it may take some time to resynchronize, depending on how many backups have been taken.

2. Modify the setupCoreData method of CoreDataHelper.m to import test data instead of setting the default store as the initial store. Delete the application from the device and allow the test data to import. Once the import is complete and the thumbnails have been automatically generated, link to Dropbox and take a backup. This will create a ~19MB ZIP file that may take a while to upload to Dropbox.


Note

Bit length overflow messages in the console log are a normal part of the image compression.


3. Log on to the Dropbox website and download the ~19MB backup ZIP file from your Apps/Grocery Dude directory. Extract and examine the contents of the ZIP file. You will likely only see the Grocery-Dude.sqlite and associated WAL and SHM files. The externally stored images are hidden.

4. Examine the contents of the extracted backup files by temporarily enabling hidden file visibility in Finder as follows:

image Open Terminal on your Mac from Applications > Utilities.

image In Terminal, execute defaults write com.apple.Finder AppleShowAllFiles YES

image In Terminal, execute killall Finder

image Examine the contents of the extracted ZIP file from step 3. The expected result is shown in Figure 13.9. The _EXTERNAL_DATA directory is the Core Data managed folder of images, stored externally from the SQLite store. If you add a .png file extension for a file, you will be able to view the file!

Image

Figure 13.9 Backup ZIP contents include _EXTERNAL_DATA structure

image In Terminal, execute defaults write com.apple.Finder AppleShowAllFiles NO

image In Terminal, execute killall Finder

Reverse the changes from step 2 of the exercises before moving to the next chapter.