PCTradeMobile

The PCTradeMobile sample demonstrates a more 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.

The sample gives an overview of the general concepts of Data Abstract for Cocoa and how to apply them in an iOS application. It is worth noting that the goal of the sample is not to push the envelope in state of the art iOS UI design, but to concentrate on DA functionality; for this reason the interface is kept simple.

Getting Started

The sample is typically located in /Developer/RemObjects Software/Samples/Data Abstract/Mobile/PCTradeMobile, 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 the Clients table from the PCTrade schema as well as manipulate the Orders table. The UI is made up of two tab views:

  • Clients tab displays all of the clients that are in the Clients table. Swiping on a client will allow you to delete it, pressing the + presents an edit window where you can add a new client and pressing on a client row allows you to edit the details of an existing client.

  • The Orders tab is more complicated as it presents data from multiple tables in one UITableView. This is achieved by using a combination of the data from the Orders table, as well as columns added using both lookup and calculated fields. It is possible to add additional orders by pressing the + button and filling in the details on the "New Order" view.
  • Pressing on an Order in the Orders table allows you to drill in and see more detail about that order which similarly uses both lookup and calculated to present relevant information pulled from multiple tables.

When the sample is run it will automatically connect to an instance of Relativity Server running on the local machine. It checks to see if there is a briefcase file available and if there is load the relevant data from it. If the file doesn't exist (for instance when the app is run for the first time) then the required data will be retrieved from the server.

Any time the data in the tables are changing (either by editing or adding new rows), the changes are automatically applied to the version stored in the server.

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.

App Structure

The sample is built around many different classes; AppDelegate, DataAccess, ClientsViewController, ClientViewController, ClientEditorViewController, OrdersViewController, OrderViewController, OrderEditorViewController, OrdersViewCell, OrderDetailPropertyCell, OrderPropertyCell, ProductPickerCell, DatePropertyEditorCell, ProductPickerViewController, PickerViewController, DetailEditorViewController and Converters.

The AppDelegate class is mostly the stock version, except that application:didFinishLaunchingWithOptions: has been overloaded to cause the DataAccess class to load the initial data.

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. Principally this section is concerned with this section.

The ClientsViewController, ClientViewController and ClientEditorViewController group of classes are concerned with displaying the table of clients, viewing a specific client and editing/adding a new client.

The OrdersViewController, OrderViewController, and OrderEditorViewController classes are used to display the orders, details of a specific order and editing/adding a new order.

The PickerViewController is used by OrderEditorViewController to present a table of Clients or Orders and select one.

The OrdersViewCell, OrderDetailPropertyCell, OrderPropertyCell and DatePropertyEditorCell classes are companion classes to table cells and provide no customization.

The DetailEditorViewController class works in conjunction with the ProductPickerViewController class to allow the user to add specific details to an order. The DetailEditorViewController view is used when the user clicks on the "Add Detail..." button.

Finally the Converters class provides a number of static methods that are used to convert one value to another; for instance to get a currency value from a string.

Retrieving a Table

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.

Inserting 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.

In this sample the addNewRowInEditMode: method is used, and the Id from the "order" table assigned to the "Order" field of the new table row.

//DataAccess.m
- (DADataTableRow *)addDetailForOrder:(DADataTableRow *)order {
    DADataTableRow *detail = [[[DataAccess sharedInstance] orderDetailsTable] addNewRowInEditMode:YES];
    detail[@"Order"] = order[@"Id"];
    return detail;
}

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 swipes to delete a client, it calls the removeClient method passing in the swiped row. That row is then passed to the removeRow method of DADataTable. At this point the row has been only removed from the local table, to remove it from the server as well the beginApplyChanges method needs to be called.

- (void)removeClient:(DADataTableRow *)clientRow {
    [self.clientsTable removeRow:clientRow];
}

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
}

Adding & using Lookup fields

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"]];

Adding & using Calculated fields

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 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...");
}