PCTradeOffice

The PCTradeOffice sample demonstrates a complex application that provides access to the PCTrade database (PCTrade schema) and allows the user to browse the content much like a "real" client application would. It covers many of the concepts demonstrated by in other samples; for instance calculated fields, lookup fields, retrieving tables with sql, using briefcases, communicating with both asynchronous & synchronous methods, as well as dynamic select and dynamic where.

Getting Started

The sample is typically located in /Developer/RemObjects Software/Samples/Data Abstract/Desktop/PCTradeOffice, though you may have installed the Data Abstract for Cocoa and it's samples in another location.

To build it, you will of course need Xcode, and like all the samples provided with Data Abstract for Cocoa you will need to be running the Relativity Server with the DASamples Domain and the PCTrade Schema available.

Running the Sample

This sample allows you to view & alter the content of a number of different tables, from the point of view of users who have different levels of access. When the sample first starts it presents the main UI and a login dialog with three users that can be selected.

Each of three users have different access levels which provide different abilities:

  • Mollie Bennet the Managing Director of the fictional company. She has access to all of the data and can view all of of the available views. In the "Orders" view she has access to all of the data unlike the other two users. She also can view the special "Sales Report" data.
  • Galle Dalton who is the Income Manager, she is only able to view the "Orders", "Price List" and "Providers". Her "Orders" table is filtered to only show the orders that she has originated.
  • Sandy Manning who is the Sales Manager, she is can only view the "Orders" and "Clients" tables.

Simply click on the user you want and then press the "Login" button. Once logged in you will have a number of the tab like views available. Each one presents data retrieved from multiple tables in the remote schema. Note not all of those views are available to every user in the system.

  • The Users tab provides a means to switch to different users, as well as see the access rights for that user. When you press the "Change user" button the login dialog is again presented.

  • The Orders view displays data retrieved from the "Orders", "OrderDetails", "PriceList" and "RestReport" tables. Many of the fields on display are added using Lookup and Calculated fields. The data returned from the server is filtered based on the user accessing the server using Business Rules Scripting and Dynamic Where.

  • The PriceList view takes data from the "Products" and "PriceList" tables are presents the data in a tree like structure, and demonstrates using NSPredicates to filter DADataTable objects.

  • The Clients view displays a table of all of the clients in the "Clients" table, and allows the user to add, edit or delete them.

  • The Providers view similarly displays a table of all of the stock providers in the "Providers" table and allows the user to add, edit or delete them.

  • The Reports view is only available to the managing director user, it uses a special "SalesReport" table in the PCTrade schema that uses custom SQL to pull together data from multiple schema tables and return that. The "SalesReport" table can be passed two parameters (a start and end date) that then filters the data based on those dates.

Examining the Code

This section gives an overview of all the classes that makes up this sample as well as specific details related to using the classes provided by Data Abstract for Cocoa. There are a total of 20 classes and 3 delegates that compromise this application.

App Structure

The sample is built around many different classes; AppDelegate, DataAccess, .

The AppDelegate class has some IBAction methods and retrieving the PreferenceController, and for causing the DataAccess to refresh the data it previously retrieved from the server.

The DataAccess class handles the interaction with the instance of Relativity Server including retrieving data, adding lookup and calculated fields and applying changes back to the server, and helper methods that filter DADataTables. Principally this section is concerned with this section.

The AppUser contains information about the logged in user, including their name, company role, and what access rights they have. It is used in a couple of places to control what is available to the current user to use (the toolbar in MainWindowController, setting the Type value in new orders in OrderEditorWindowController).

The MainWindowController controls the main user interface for the sample. It handles enabling / disabling the buttons on the toolbar based on which user is logged in, it handles switching users, performing searches, updating the tables in DADataTableController when the data is retrieved from the server.

The LoginWindowController is presented by the MainWindowController as a modal dialog to allow the user to choose a user to log in as. The DataAccess class acts as a delegate, and its loginWithUserName:AndPassword: is called when the LoginWindowController checks if the login data is valid.

The PreferenceController handles displaying and editing the user preferences, and is initiated from the MainWindowController. Here the Server Address can be changed from the default of "http://localhost:7099/bin", doing so updates the shared user defaults controller.

The EditorWindowController is a base class for handling editing data including populating popups, binding data to the popup, displaying the editor view and handling "OK" and "Cancel" actions. The following classes inherit it, though only the last two provide any specialised overriding ClientEditorWindowController, ProviderEditorWindowController, OrderEditorWindowController and SelectProductWindowController.

The ClientEditorWindowController simply subclasses the EditorWindowController, the class itself has no specialization. The associated xib has a UI for editing the details of a client.

The ProviderEditorWindowController class is also a simple subclass of EditorWindowController. The associated xib has a UI for editing the details of a provider.

The OrderEditorWindowController is a more complex subclass of EditorWindowController which presents a row of data from the "Orders" table; including the fields from the lookup & calculated fields. The class handles populating a table & popups and filtering data from a DADataTable using an NSPredicate.

The SelectProductWindowController is used by the OrderEditorWindowController to present a table of available products that the order being edited could contain. When editing is complete these selected rows are passed back to OrderEditorWindowController which in turn updates the appropriate table row.

The ListViewController is a base class for displaying table data using NSTableViews and DADataTableController as well as handling the basic actions for adding and editing. The following classes inherit this class: ClientsViewController, OrdersViewController and ProvidersViewController.

The ClientsViewController handles the presentation of clients from the "Clients" table. The main thing of note is that it uses a DADataTableController for binding the data to the UI elements, and that when a value is entered into the search field a NSPredicate is generated and passed to the setFilterPredicate method of DADataTableController.

OrdersViewController handles the presentation of orders from the "Orders" table, along with some lookup & calculated fields which pull data from the "OrderDetails", "PriceList" and "RestReport" tables. The main thing of note is that it uses a DADataTableController for binding the data to the UI elements, and that when a value is entered into the search field a NSPredicate is generated and passed to the setFilterPredicate method of DADataTableController.

ProvidersViewController handles the presentation of providers from the "Providers" table. The main thing of note is that it uses a DADataTableController for binding the data to the UI elements, and that when a value is entered into the search field a NSPredicate is generated and passed to the setFilterPredicate method of DADataTableController.

The ImageAndTextCell class overrides NSTextFieldCell to provide a cell that presents both an image and text. It is used by the OrderEditorWindowController, OrdersViewController, PricelistViewController and SelectProductWindowController.

The ImageHelper class is used to load png files from disk for use in the UI. The only class that makes use of this is OrdersViewController, which displays different images depending on a row's order "Status" field.

The PricelistViewController and ProductNode work together to present a tree view of all of the available products, their prices and quantity available. From a Data Abstract perspective it shows for to add a new row to a table, how to remove a row and how to filter a DADataTable using an NSPredicate.

The ReportsViewController class takes data from specified dates from a special table in the server schema and presents it in a bar graphic. The main thing of note is that it uses the buildReportWithDateRange: method of DataAccess to build a special request for data between certain dates.

Retrieving Data

To retrieve a copy of a table from the schema you can use one of the multiple getDataTable methods that are provided by the DARemoteDataAdapter class.

This sample uses four of the variants, specifically getDataTable: which just takes the name of the table to retrieve, getDataTables which takes an array of DARemoteDataAdapters to retrieve, getDataTableWithSQL: to retrieve tables based on the passed DA SQL statements and then finally getDataTables:withSQL: which returns an array of the requested tables and according to the given DA SQL.

The getDataTable* methods communicate with the server in a synchronous fashion, there are also asynchronous variants of all of the above methods. To learn more about those see the API documentation for DARemoteDataAdapter.

NOTE Passing an invalid table name to the getDataTable method will result in an ROException being thrown which should be handled accordingly.

//DataAccess.m
- (void)beginLoadClients {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        DADataTable *t = [rda getDataTable:@"Clients"];
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Loaded %@", @"Clients");
            self.clientsTable = t;
            [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_DATA_READY object:self userInfo:@{INFO_CHANGED_TABLES: @[@"Clients"]}];
            [self saveDataInBackground];
        });
    });
}

Retrieving a table with DA SQL

A DA SQL statement is simply a normal SQL statement that uses the table & field names from the schema rather than the ones in the back-end Database. There is no special class required, simply put the DA SQL statement in an NSString and pass it to the relevant method. DA SQL statements aren't limited to basic requests, you can compose as complex a request as you need by using a multi-line NSString.

Here a DA SQL statement is created that will retrieve user information for the currently logged in user from the "Users" table. The DA SQL statement is passed as the first argument to the beginGetDataTable:withSQL:withBlock: method; the second argument is the block to be executed upon the completion of the method call. Upon completion the retrieved data is stored in an instance of the AppUser class, and a notification dispatched which the MainWindowController is listening for, this notification is used to switch the UI over to the newly logged in user and enabling / disabling the appropriate toolbar items.

//DataAccess.m
-(DAAsyncRequest *)loadCurrentUserInfo:(NSString *)userLoginName {
    if (userLoginName.length == 0)
        @throw [NSException exceptionWithName:@"Cannot load current user info" reason:@"User login name is not specified" userInfo:nil];

    NSString *sql = [NSString stringWithFormat:@"SELECT [t0].[UserId],[t0].[UserName], [t0].[Login], [t0].Photo, [t0].[RoleName], [t0].[CanManageOrderTypes],[t0].[CanManagePrices],[t0].[CanManageProviders], [t0].[CanManageClients],[t0].[CanSeeReports] FROM [users] [t0] WHERE [t0].[Login] = '%@'", userLoginName];

    DAAsyncRequest *request = [rda beginGetDataTableWithSQL:sql withBlock:^(DADataTable *table) {

        DADataTableRow *row = [table rowAtIndex:0];

        _userInfo = nil;
        _userInfo = [[AppUser alloc] initWithName:row[@"UserName"]
                                         andLogin:row[@"Login"]
                                          andRole:row[@"RoleName"]];

        _userInfo.userId = [row[@"UserId"] integerValue];
        _userInfo.canManageOrderTypes = [row[@"CanManageOrderTypes"] integerValue];
        _userInfo.canManagePrices = [row[@"CanManagePrices"] integerValue];
        _userInfo.canManageProviders = [row[@"CanManageProviders"] integerValue];
        _userInfo.canManageClients = [row[@"CanManageClients"] integerValue];
        _userInfo.canSeeReports = [row[@"CanSeeReports"] integerValue];
        _userInfo.userImage = [[NSImage alloc] initWithData:row[@"Photo"]];


        [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_CURRENT_USER_CHANGED
                                                            object:self];
    }];
    [request setDelegate:self];
    return request;
}

Adding a row

Each table is made up of multiple DADataTableRow objects. Each one representing a row in the schema table and its columns. To add a new row to the table, you can use the either the addNewRow: or the addNewRowInEditMode: factory methods of DADataTable. Each appends a new row to the table and returns a reference to it, the difference is that in edit mode that new row won't be visible in the table until the changes are posted. The values for all of the columns will be initially set to null, except for any columns that use an AutoInc value in which case they will be assigned a negative value. The Id will be updated to the actual Id value after the changes have been applied to the server.

//ListViewController.m
-(IBAction)addAction:(id)sender {
    NSLog(@"BASE addAction:");
    id data = [self.tableController.table addNewRowInEditMode:YES];
    [self editRow:data];
}

Deleting a row

To remove a row from a table you need to use the removeRow method which takes the DADataTableRow you want to remove. The change is added to the changedRows list, which is a list of all DADataTableRows that have modifications.

There is a variant of removeRow, removeRowAtIndex, that takes an index for the row you wish to delete; note that this index value is simply the position the row has in the table (starting from 0) and has no relation to any id value.

Here when the user presses the delete button, it calls the removePrice method and establishes the currently selected row. That row is then passed to the removeRow method of DADataTable. Remember at this point the row has been only removed from the local table, to remove it from the server the changes would also need to be applied to the server.

// PricelistViewController.m
- (IBAction)removePrice:(id)sender {

    id price = [[self.pricesController selectedObjects] lastObject];
    DADataTable *priceTable = [[DataAccess sharedInstance] priceListTable];
    [priceTable removeRow:price];
    [[self prices] removeObject:price];
    [self.pricesController rearrangeObjects];
}

Applying Changes to the server

As noted earlier all of the changes made like adding, editing or deleting a row are only made on the local copy of the table until we apply those changes to the server. To do that we use the beginApplyChangesForTables:start: of DARemoteDataAdapter, it takes an array of DADataTables and extracts a delta of changes made to the table and passes that to the server when the DAAsyncRequest is started. A delta of changes are then received from the server and merged into the local table.

Here applying updates is spread through three methods. The first is beginApplyUpdates which adds any tables that have changes (determined by using the hasChanges property) and then passes that to beginApplyChangesForTables:start: which returns a DAAsyncRequest object which will handle the communications. The DataAccess class is set as the delegate of the DAAsyncRequest and it implements the asyncRequest:didFailToApplyChange:forTable: & asyncRequest:didFinishApplyingChangesForTables:withErrors: methods which will be called as appropriate when the DAAsyncRequest completes.

//DataAccess.m
- (DAAsyncRequest *)beginApplyUpdates {

    if (busy) {
        return nil;
    }

    NSMutableArray *a = [NSMutableArray array];

    // references ...
    if (self.clientsTable.hasChanges) [a addObject:self.clientsTable];
    if (self.providersTable.hasChanges) [a addObject:self.providersTable];
    if (self.productsTable.hasChanges) [a addObject:self.productsTable];

    // main tables ...
    if (self.priceListTable.hasChanges) [a addObject:self.priceListTable];
    if (self.ordersTable.hasChanges) [a addObject:self.ordersTable];
    if (self.orderDetailsTable.hasChanges) [a addObject:self.orderDetailsTable];

    if ([a count] == 0) {
        NSLog(@"Nothing to update?!");
        return nil;
    }

    DAAsyncRequest *ar = [self.dataAdapter beginApplyChangesForTables:a start:NO];
    [ar setDelegate:self];
    [ar setContext:@"AppyUpdates"];

    [self setBusy:YES];
    [ar start];
    return ar;
}

- (void)asyncRequest:(DAAsyncRequest *)request didFailToApplyChange:(DADeltaChange *)change forTable:(DADataTable *)table {
    [self setBusy:NO];

    NSString *message = [NSString stringWithFormat:@"Failed to apply %@ to server: %@", [table name], [change changeMessage]];

    NSLog(@"%@", message);
    [self.delegate alertError:message];

    //ToDo: do any additional error handling for the entire request
}

- (void)asyncRequest:(DAAsyncRequest *)request didFinishApplyingChangesForTables:(NSArray *)tables
          withErrors:(NSArray *)errors {

    [self saveDataInBackground];
    [self setBusy:NO];

    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_TABLES_CHANGED
                                                        object:self
                                                      userInfo:@{NOTIFICATION_TABLES_CHANGED_ARG: tables}];

    if (errors) {
        //ToDo: do any additional error handling for the entire request
    }
    //ToDo: do any additional handling for the completed request
}

Using a Lookup Field

There are two ways to add a lookup field, the first is by using the addLookupFieldName:sourceField:lookupTable:lookupKeyField:lookupResultField: method of DADataTable, the other way is to manually define a DALookupFieldDefinition object and pass it to the table using the addLookupField: method.

Here the addLookupFieldName:sourceField:lookupTable:lookupKeyField:lookupResultField: method is used, which takes the following arguments:

  • Name a unique name to be given to the lookup field.
  • sourceField the field that will be used as the key value for matching against the lookupKeyField
  • lookupTable takes a reference to the lookup table
  • lookupKeyField the key field in the lookupTable that will be used to match against the sourceField
  • lookupResultField the field in the lookup table that will be used as the result when the sourceField and lookupKeyField match

In the code below it is adding a new lookup field with the name "StatusName", whose source field is the Status field of the Orders table, this will act as the key we will use to look up a value. The "OrderStatus" table is assigned as the lookup table and the Id field is field we are going to match against the Status field. If a match is found the value in the Name field of the appropriate row is returned.

//DataAccess.m - setupData method
[self.ordersTable addLookupFieldName:@"StatusName"
                         sourceField:[self.ordersTable fieldByName:@"Status"]
                         lookupTable:self.orderStatusTable
                      lookupKeyField:[self.orderStatusTable fieldByName:@"Id"]
                   lookupResultField:[self.orderStatusTable fieldByName:@"Name"]];

Using a Calculated Field

To add a calculated field to a DADataTable you need to either use the addCalculatedFieldName:dataType:target:selector: method of DADataTable, or manually create an instance of DACalculatedFieldDefinition and add it using the addCalculatedField: method.

Here we use the addCalculatedFieldName:dataType:target:selector: method which takes 4 arguments:

  • Name - which is the unique name to be given to the field/column
  • dataType is the data type for the field which should be one of the types defined in DADataType
  • target is the object that will provide the method used for calculating the value of this field
  • selector is the method/selector on the target that will be called to calculate the fields value.

In the sample below, a new calculated field is added to the OrderDetails table, with the name "Sum", which will use the "datCurrency" data type which is used for monetary values, the target is "self" (the DataAccess class) and the calculateSumForDetailRow: is the selector that will be called. The calculateSumForDetailRow: method takes the value from the "Amount" field and multiplies it by the value in the "Price" field and returns that result.

//DataAccess.m - setupData method
[self.orderDetailsTable addCalculatedFieldName:@"Sum"
                                          dataType:datCurrency
                                            target:self
                                          selector:@selector(calculateSumForDetailRow:)];
//DataAccess.m
-(id)calculateSumForDetailRow:(DADataTableRow *)row {

    NSInteger amount = [row[@"Amount"] integerValue];
    double price = [row[@"Price"] doubleValue];
    double sum = amount * price;

    return [NSNumber numberWithDouble:sum];
}

Using a DADataTableController

The DADataTableController is an extension of a NSArrayController that is used to manage a DADataTable object and display its contents in an NSTableView. It is principally used in the sample in the ListViewController base class that the ClientsView, OrdersView and ProvidersView inherit from.

Filtering with NSPredicate

The DADataTable class provides the rowsFilteredUsingPredicate: helper method that allows you to quickly filter the table to get an NSArray of exactly the records you are interested in. Simply create the NSPredicate object you need and pass it as an argument. Note that there are similar methods available that allow you to return an array of sorted results, and the direction in which those records are sorted.

Here we want to get the value from the Sum field (which is a calculated field) from an order row that matches orderID.

//DataAccess.m
-(id)calculateSumForOrderRow:(DADataTableRow *)row {

    NSInteger orderId = [row[@"Id"] integerValue];

    double sum = [[[self.orderDetailsTable rowsFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"Order == %d", orderId]] valueForKeyPath:@"@sum.Sum"] doubleValue];

    return [NSNumber numberWithDouble:sum];
}

Retrieving a table with filter parameters

This is a more advanced feature of Data Abstract and Remoting SDK that allows you to pass parameters to a DADataTable that in a schema but is not an actual table in the underlying database. The schema table uses a custom sql statement to pull fields from multiple tables, however it takes two input parameters which were defined in the schema. The input parameters are called pStartDate and pEndDate, and are defined as having the DateTime type, and are used to narrow down the data request.

The code below can be broken down into 4 sections where the first two are creating the parameters that will be passed to the server. A DataParameter is created and the Name property is assigned the name of the parameter you are interested in; for example "pStartDate". Then for the Value property you pass an instance of ROVariant, which is initalised with the a value representing the data type set in the schema (the types are available in ROVariant.h of the Data Abstract for Cocoa SDK and in the ROVariantTypeCode documentation).

The third section defines a TableRequestInfo object that allows you to tune the amount of data returned by your request and also includes the parameters you are interested in. To send the parameters they need to be wrapped in an DataParameterArray object.

The last section defines a DataAbstractService_Proxy (which implements the IDataAbstractService protocol) that is used to retrieve the data from the server using the GetData:: method. The streamer is then initialised with the returned raw data, and decoded and assigned to a DADataTable object.

//DataAccess.m
- (DADataTable *)buildReportWithDateRange:(NSArray *)dates {

    NSString *reportName = @"SalesReport";
    DataParameter *start = [[DataParameter alloc] init];
    start.Name = @"pStartDate";
    start.Value = [[ROVariant alloc] initWithDataType:varDateTime value:dates[0]];

    DataParameter *end = [[DataParameter alloc] init];
    end.Name = @"pEndDate";
    end.Value = [[ROVariant alloc] initWithDataType:varDateTime value:dates[1]];

    TableRequestInfo *request = [[TableRequestInfo alloc] init];
    request.IncludeSchema = YES;
    request.MaxRecords = -1; // all records
    request.Parameters = [[DataParameterArray alloc] initWithNSArray:@[start, end]];

    DABin2DataStreamer *streamer = (DABin2DataStreamer *)rda.dataStreamer;
    RORemoteService *remoteService = rda.dataService;
    DataAbstractService_Proxy *service = [[DataAbstractService_Proxy alloc] initWithService:remoteService];
    NSData *rawData = [service GetData:[StringArray arrayWithNSArray:@[reportName]]
                                      :[TableRequestInfoArray arrayWithNSArray:@[request]]];
    DADataTable *reportTable = [[DADataTable alloc] init];
    reportTable.name = reportName;
    [streamer initializeStreamerWithMode:smReadingFromBegin andData:rawData];
    [streamer readDataTable:reportName toTable:reportTable];
    return reportTable;
}

Using Briefcases

This sample makes use of Briefcases to load and save data that was previously retrieved from the "PCTrade" schema.

When the app starts it calls threadedLoadInitialData to get the data needed by this sample. It tests to see if the briefcase file exists, if not a fresh set of data is retrieved from the server and then saved to disk. Otherwise the briefcase is loaded with the briefcaseWithFolder: method and the BRIEFCASE_DATA_VERSION_KEY property retrieved by passing the name of the required property to briefcase.properties. If the property returned doesn't match BRIEFCASE_DATA_VERSION then the briefcase is deleted from disk using the delete method and the data retrieved from the server, otherwise the tables are retrieved with a call to the loadDataFromBriefcase: of DataAccess.

//DataAccess.m
- (void)threadedLoadInitialData {
    @autoreleasepool {
        @try {
            NSFileManager * m = [NSFileManager defaultManager];
            if (![m fileExistsAtPath:briefcaseFileName]) {
                // No briefcase found. Downloading fresh references data...
                [self reloadReferences];
                [self saveData];
            } 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 reloadReferences];
                    [self saveData];
                } else {
                    [self loadDataFromBriefcase:briefcase];
                }
            }
            [self reloadOrdersData];
            [self setupData];
            [self performSelectorOnMainThread:@selector(triggerDataReady) withObject:nil waitUntilDone:NO];
        }
        @catch (NSException * e) {
            NSLog(@"Exception downloading initial data: %@", e);
            [(id)self.delegate performSelectorOnMainThread:@selector(alertError:) withObject:[e description] waitUntilDone:YES];
        }
    }
}

After the briefcase has been loaded by using briefcaseWithFolder:, the tables can be easily retrieved using the tableNamed: of DABriefcase.

//DataAccess.m
- (void)loadDataFromBriefcase:(DABriefcase *)briefcase {

    NSLog(@"Start loading briefcase...");
    self.providersTable = [briefcase tableNamed:@"Providers"];
    self.workersTable = [briefcase tableNamed:@"Workers"];
    self.clientsTable = [briefcase tableNamed:@"Clients"];
    self.productGroupsTable = [briefcase tableNamed:@"ProductGroups"];
    self.productsTable = [briefcase tableNamed:@"Products"];
    self.orderStatusTable = [briefcase tableNamed:@"OrderStatus"];
    self.orderTypeTable = [briefcase tableNamed:@"OrderType"];
    NSLog(@"End loading briefcase");    
}

To save data in a briefcase we need to use the writeBriefcase: method of DABriefcase. When using a folder based briefcase you could save each table as needed by using the writeTable: and writeTables: methods of DAFolderBriefcase instead.

Here the briefcase is loaded with briefcaseWithFolder: method of DABriefcase will attempt to load into memory the briefcase folder if it exists, otherwise it will create it in memory until such time as you write it to disk.

As you can see in the saveDataToBriefcase method, we add multiple tables to the briefcase using the addTable: method. Alternatively addTables:, which takes any array of tables, could be used. If a table with the same name is already there, it will be replaced with this new table.

The briefcase can have properties set which are stored in a dictionary, so we can set and retrieve a property by passing its name to the properties object. Here we set a version number which could be used to prevent loading a briefcase from an old version of the application.

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

- (void)saveDataToBriefcase:(DABriefcase *)briefcase {

    NSLog(@"Start saving briefcase...");
    [briefcase addTable:self.providersTable];
    [briefcase addTable:self.workersTable ];
    [briefcase addTable:self.clientsTable];
    [briefcase addTable:self.productGroupsTable];
    [briefcase addTable:self.productsTable];
    [briefcase addTable:self.orderStatusTable];
    [briefcase addTable:self.orderTypeTable];

    [briefcase.properties setValue:BRIEFCASE_DATA_VERSION forKey:BRIEFCASE_DATA_VERSION_KEY];
    NSLog(@"End saving briefcase...");
}