Adding Briefcase Support (Objective-C, Mac/iOS)

In the previous tutorial you created a ToDo List application which pulled its data from a Relativity Server instance and any changes you make to the data are passed directly back to the server.

No data is persisted locally, so when the user quits the app and later uses it again, the app needs to retrieve the data from the server again. If the network connection or the server is unreachable the user won't be able to use the app, which is obviously an unacceptable situation.

In this tutorial we will expand the ToDo List application to be able to store and load local data using a feature of Data Abstract that we call Briefcase support. Afterwards will explore how and where the data is saved.

What exactly is Briefcase support? It is a mechanism for storing a local snapshot of the data, to use when your application either cannot or should not connect to the Relativity Server. This serves a few purposes

  1. Beyond the initial start, the app will be able to launch faster as it can load the local data without having to talk to the server first.

  2. The application could request only the changed records for some huge table from the server side and then merge the delta with the client table loaded from the local briefcase.

  3. The application can be used without a connection to the server, for example when the user is out of reach of WiFi or a cellular network. Any changes the user makes to the data will persist locally and then using Data Abstract's robust conflict resolution those changes can be sent to the server at a later time.

As the ToDo List application is already nicely encapsulated, all of the changes you will need to make will be in the DataAccess class. This also means that the changes discussed in this article would apply equally to the iOS and the Mac versions of the app.

As we have created the application using the DataAbstract Xcode template much of the legwork for using Briefcase support is already in place. You simply need to specify what data is to be saved and loaded.

How do we start? By ...

Saving Data to a Briefcase

As a first step we need to specify what data we actually want to save in the briefcase. In the previous tutorial we commented out the saveDataToBriefcase: from the DataAccess class. Here we shall implement the method to add each of the DADataTables that we want to store to the briefcase. For the ToDo List application that is the Priorities and Tasks tables.

//DataAccess.h
- (void)saveDataToBriefcase:(DABriefcase *)briefcase
{
    [briefcase addTable:self.prioritiesTable];
    [briefcase addTable:self.tasksTable];
    // Uncomment this to finalize briefcase support
    // (without this value written, DataAccess will ignore the briefcase when starting up, see threadedLoadInitialData)
    [briefcase.properties setValue:BRIEFCASE_DATA_VERSION forKey:BRIEFCASE_DATA_VERSION_KEY];
}

The last line is particularly important as it adds a data version to the briefcase which is used when the app tries to load the data from the briefcase. If it isn't specified the data in the briefcase will be ignored and the data will be freshly downloaded from the server.

An added benefit of using the BRIEFCASE_DATA_VERSION property is that you can make changes to the database or data structure you use and not worry about the user loading out of date dataformats from their briefcase. When you make changes that would conflict with data that is stored in a users briefcase simply increment value of BRIEFCASE_DATA_VERSION in DataAccess.m. Then the next time the application runs it will see a version mismatch and won't try to load the out of date data. Instead it will pull fresh data from the server.

Next we will actually save the briefcase with the data that is initially downloaded from the server by the loadInitialData method. To do this simply add a call to saveDataInBackground todownloadData after the table properties have been set.

- (void)downloadData
{
    NSDictionary *tables = [rda getDataTables:@[@"Priorities", @"Tasks"]];
    self.prioritiesTable = tables[@"Priorities"];
    self.tasksTable = tables[@"Tasks"];
    
    [self saveDataInBackground];
}

Loading Data Back from a Briefcase

Next we need to tell the application what data it is to extract from the briefcase. In the previous tutorial we commented out loadDataFromBriefcase in theDataAccess class, now we will uncomment it and extract the Priorities table and the Tasks table. To get the data from the briefcase we call the method tableNamed passing in the name of the table, and storing the results in our table properties.

- (void)loadDataFromBriefcase:(DABriefcase *)briefcase
{
    self.prioritiesTable = [briefcase tableNamed:@"Priorities"];
    self.tasksTable = [briefcase tableNamed:@"Tasks"];
}

Thats it. To test it run your newly modified app one time to allow the data to be saved to the briefcase. Then quit Relativity Server and restart the app. You will see that it still launches and shows the users tasks because the data has been loaded from the briefcase.

Polishing the app

This tutorial has covered the basics of briefcase support. To make the app more polished, there are a few more changes you will want to do that we'll leave as an exercise to the reader. These are:

  1. Right now, as changes get made, the app will still always try and send the data to the server, and show an error if that failed. A polished app would first check for network connectivity, and gracefully handle this case – either by not showing an error or none at all, or by showing a more friendly error message saying, for example, "We could not reach the server, but your data is safe; we'll try to upload it again later". The app could also show a visual indicator in the user interface to show tasks that have un-uploaded changes – you can use the hasChanges property of DADataTableRow for that purpose.

  2. Since the app now has the ability to contain changes that have not been sent to the server yet, you must handle this somehow. For instance you could either provide UI, like an extra button, that lets the user try and send those changes again, later. Another possibility is to submit the data, without user interaction, to the server. Note care should, probably, be taken to make sure all changes are uploaded before refreshing data from the server otherwise the local data will be replaced. You can use hasChanges on the DADataTable to check for that, before downloading.

  3. At present the users data is only saved to the briefcase after it has been loaded from the server or when the user quits the app. You could additionally save the data when the user adds a task, edits a task, deletes a task and reloads the tasks from the server.

Finally if you are interested in what is going on in the DataAccess class to make saving and loading of the briefcase happen and where that data is stored, then carry on to the next section.

Exploring Saving and Loading

In the DataAccess class provided by the Data Abstract template the code for loading and saving the briefcase is spread over a number of methods. In this section of the tutorial we will work through the mechanics to get a better understanding of whats going on.

The first step is to determine the location to save and load the data from. This is stored in the variable briefcaseFilename which is initialized in the init method ofDataAccess`.

NSFileManager *m = [NSFileManager defaultManager];
NSArray *a = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
homeFolder = [[a objectAtIndex:0] stringByAppendingPathComponent:APPLICATION_SUPPORT_FOLDER];
[m createDirectoryAtPath:homeFolder withIntermediateDirectories:YES attributes:nil error:nil];
NSLog(@"Home folder is %@", homeFolder);

// set up briefcase
briefcaseFileName = [homeFolder stringByAppendingPathComponent:BRIEFCASE_FILENAME];

We retrieve the application support directory location and append to it the value specified for APPLICATION_SUPPORT_FOLDER (at the top of DataAccess.m). This is stored in the NSString variable `homeFolder.

The briefcaseFileName is then created by appending the value from BRIEFCASE_FILENAME to the path in homeFolder.

What happens when Saving

When the method saveDataInBackground: is called it fires of a background thread using performSelectorInBackground: to call saveData.

- (void)saveData
{
    DABriefcase *briefcase = [DABriefcase briefcaseWithFolder:briefcaseFileName];
    [self saveDataToBriefcase:briefcase];
    [briefcase writeBriefcase];
}

saveData creates a DABriefcase instance using the factory method briefcaseWithFolder: which is passed the briefcaseFileName we specified in the init method. Note there are two different types of Briefcase formats which we will discuss later.

The briefcase object is then passed to the saveDataToBriefcase: method we defined earlier which adds the tables we want to have saved.

We finally write the briefcase to storage (be it disk or flash) by calling the writeBriefcase method.

The provided template also saves the user's data when the user quits that app by implementing applicationWillTerminate: in AppDelegate.m

- (void)applicationWillTerminate:(NSNotification *)aNotification
{
    // Save data if appropriate
    [[DataAccess sharedInstance] saveData];
}

What happens when Loading

When the app starts it calls DataAccess.m's loadInitialData from AppDelegate.m's applicationDidFinishLaunching:. This in turn calls threadedLoadInitialData via a background thread.

threadedLoadInitialData first checks to see if a briefcase already exists and if it doesn't, then it downloads the data from the server.

NSFileManager *m = [NSFileManager defaultManager];
if (![m fileExistsAtPath:briefcaseFileName])
{
    // No briefcase found. Downloading fresh data...
    [self downloadData];
}

If the file does exist a new DABriefcase object is created using the data from disk. Then the BRIEFCASE_DATA_VERSION property from the briefcase is compared to the BRIEFCASE_DATA_VERSION specified in DataAccess.m and if they don't match the local briefcase is deleted and the data is downloaded instead from the server.

If the BRIEFCASE_DATA_VERSION's do match then the loadDataFromBriefcase method we defined earlier in this tutorial is called passing in the DABriefcase object, from which we extract the Priorities & Tasks tables.

else
{
    DABriefcase *briefcase = [DABriefcase briefcaseWithFolder:briefcaseFileName];
    if ([BRIEFCASE_DATA_VERSION compare:[briefcase.properties  valueForKey:BRIEFCASE_DATA_VERSION_KEY]] != NSOrderedSame)
    {
        // Briefcase out of date. Downloading fresh data...
        [briefcase delete];
        [self downloadData];
    }
    else
    {
        [self loadDataFromBriefcase:briefcase];
    }
}

Briefcase types

Briefcases can be stored in two formats: folders or file. Briefcase folders are stored as packages, with each individual table stored as a separate file. Briefcase files on the other hand contain all the tables in one single file.

Which option to choose is largely a matter of taste. Briefcase folders have the advantage that individual tables can be read, written or added independently and more efficiently, without rewriting all data. Briefcase files are easier to handle by the end-user (if your app is designed around the user dealing with them as files), because they are self contained.

To use folders we create the DABriefcase object with briefcaseWithFolders, which is the form used in the Data Abstract project template.

DABriefcase *briefcase = [DABriefcase briefcaseWithFolder:briefcaseFileName];

To use a file simply create the DABriefcase object with briefcaseWithFile

DABriefcase *briefcase = [DABriefcase briefcaseWithFile:briefcaseFileName];