Avatar of Alex

by

Creating a Simple iOS Client for Relativity Server

September 23, 2011 in iOS, Relativity, Xcode

Today I want to talk about how to create a simple iPhone client application for Relativity server using the DataAbstract for Xcode templates. Though the focus will be on Xcode 4, the same mechanisms can be applied to Xcode 3.

Going through a set of actions, we will use a DataAbstract iOS Application template to create an application with several views representing a Master/Detail relation.

Let’s start with the first step.

Creating a new project using a DataAbstract iOS Application template

Open your Xcode application and perform the File>New>New project… command.
In the opened sheet, choose the RemObjects Software category from the iOS templates section and select the Data Abstract iOS Application template.

Specify the product name and don’t forget to set the Talk to Relativity Server option.

Finally, select the destination folder for your new project and press the Create button. Your new project should now be open:

Let’s take a few seconds to review what you have at the moment. You should have a compilable application (with some warnings – we will review them a bit later) that has an AppDelegate class and a RootViewController with its xib. There should also be a DataAccess class, which will be responsible for all manipulation with data. The new project also has all the necessary linker flags and properly configured paths as shown in the screen-shot below:

Implementation of the iOS client

Now you can compile the application, after which you will see a set of warnings:

These warnings were generated by the template system to highlight places that require adjusting or proper implementation to get your application working. Let’s walk through all of them and eliminate them one by one.

1. Adjusting the connection configuration

First, let’s change some defaults in the generated DataAccess class to specify the Relativity server URL, domain and schema. For this example, let’s assume that you have the Relativity server running on a Windows computer with the IP 192.168.0.150.

Open the DataAccess.m file and specify the proper URL for the Relativity server, domain and schema.
You can comment the warning there, since we are done with it (as shown below).

//#warning Define your server address and Relativity domain name, below.
#define SERVER_URL		@"http://192.168.0.150:7099/bin"
#define SERVICE_NAME		@"DataService" // Do not change when using Relativity server
 
#define RELATIVITY_DOMAIN	@"PCTrade Sample"
#define RELATIVITY_SCHEMA	@"PCTrade"

2. Defining tables you want to work with

Since the DataAccess class encapsulates all functionality needed for working with data, you need to define your tables there. Let’s define them as retain properties. See the code below:

@interface DataAccess : NSObject <DARemoteDataAdapterDelegate, DAAsyncRequestDelegate>
{
    ...    
    DADataTable *clientsTable;
    DADataTable *ordersTable;
    DADataTable *orderDetailsTable;
}
 
@property (retain) DADataTable *clientsTable;
@property (retain) DADataTable *ordersTable;
@property (retain) DADataTable *orderDetailsTable;

Don’t forget to add the proper @syntesize directive and release the properties in the dealloc method in the DataAccess.m file.

#pragma mark -
#pragma mark Properties
@synthesize clientsTable, ordersTable, orderDetailsTable;
...
- (void) dealloc {
    ...
    [clientsTable release], clientsTable = nil;
    [ordersTable release], ordersTable = nil;
    [orderDetailsTable release], orderDetailsTable = nil;
 
	[super dealloc];
}

3. Implementing the initial loading of data

Now let’s implement the method for the initial loading of data from the Relativity server.
Locate the downloadData method of the DataAccess class and add the following code:

Note: Our templates contain various vital tips and explanations on how to use a particular method. Please carefully review this kind of information, it can be very useful.

- (void)downloadData {
	//#warning Implement this method to download any data your application needs on first start
 
	NSArray *tableNames = [NSArray arrayWithObjects: 
                           CLIENTS_TABLE_NAME, 
                           ORDERS_TABLE_NAME, 
                           ORDERDETAILS_TABLE_NAME, nil];
 
	NSDictionary *tables = [rda getDataTables:tableNames];
 
	[self setClientsTable:[tables valueForKey:CLIENTS_TABLE_NAME]];
	[self setOrdersTable:[tables valueForKey:ORDERS_TABLE_NAME]];
	[self setOrderDetailsTable:[tables valueForKey:ORDERDETAILS_TABLE_NAME]];
}

Note that we used a synchronous call to getDataTables here, because the whole downloadData method will be executed in a background thread (i.e. asynchronously).

4. Adding briefcase support to your application

In order to allow your application to work in offline mode, you need to persist downloaded data somewhere on the device. Let’s use the DABriefcase functionality for this.
Locate the loadDataFromBriefcase: and saveDataToBriefcase: methods in the DataAccess class and implement them as shown below:

- (void)loadDataFromBriefcase:(DABriefcase *)briefcase {
 
	//#warning Implement this method to re-load your data from the briefcase when re-launched
 
	[self setClientsTable:[briefcase tableNamed:CLIENTS_TABLE_NAME]];
	[self setOrdersTable:[briefcase tableNamed:ORDERS_TABLE_NAME]];
	[self setOrderDetailsTable:[briefcase tableNamed:ORDERDETAILS_TABLE_NAME]];
}
 
- (void)saveDataToBriefcase:(DABriefcase *)briefcase {
	//#warning Implement this method to save your data to the briefcase
 
	[briefcase addTable:[self clientsTable]];
	[briefcase addTable:[self ordersTable]];
	[briefcase addTable:[self orderDetailsTable]];
 
	// 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];
}

5. Extending the Orders table with lookup and calculated fields

Let’s look at the orders table structure. It only has a minimal set of the fields like id, client id, date and status. That’s not a lot and it would be be great to see (for example) the customer name instead of its ID and the total sum for the given order (which could be calculated from the OrderDetails table). Toachieve this, you’ll have to add lookup and calculated fields to the Orders table. Locate the setupData method of the DataAccess class and add the following implementation:

- (void)setupData {
 
	// Use this method to implement any setup that's needed on your data tables, such as
	// creating lookup or calculated fields, etc.
 
        [[self ordersTable] addCalculatedFieldName:@"OrderTotal" 
                                          dataType:datCurrency
                                            target:self
                                          selector:@selector(calculateSumForOrder:)];
 
	[[self ordersTable] addLookupFieldName:@"CustomerName" 
                                   sourceField:[[self ordersTable] fieldByName:@"Client"] 
                                   lookupTable:[self clientsTable]
                                lookupKeyField:[[self clientsTable] fieldByName:@"ClientId"] 
                             lookupResultField:[[self clientsTable] fieldByName:@"ClientName"]];
}
 
-(id)calculateSumForOrder:(DADataTableRow *)row {
 
	double result = 0;
	NSPredicate *p = [NSPredicate predicateWithFormat:@"Order == %@", [row valueForKey:@"OrderId"]];
	NSArray *rows = [[orderDetailsTable rows] filteredArrayUsingPredicate:p];
	for (DADataTableRow *orow in rows) {
		double summ = [(NSNumber *)[orow valueForKey:@"Total"] doubleValue];
		result = result + summ;
	}
	return [NSNumber numberWithDouble:result];
}

6. Checking the login and password stuff in your Application delegate

Relativity server provides secured services protected by login and password. Thus, when asking for data the first time, a SessionNotFound exception will be raised on the Relativity side to tells you that you need to login to the Relativity domain first. When you log in to it, it will be able to find your registered session and allow you to proceed. The iOS client application can catch this kind of exception and provide a login operation in the background, and, after successful login, it will repeat the last failed request. When you open the PCTradeClientAppDelegate.h file you will see that this class supports the DataAccessDelegate protocol, which has a needLogin:password: method that will be used for obtaining the proper login and password.
Since you are mostly using default settings, you can leave the already generated method untouched. It should look like this:

- (BOOL)needLogin:(NSString **)login password:(NSString **)password {
 
	//#warning Implement this method to ask the user for login information when needed
 
	// The DataAccess automatically takes care of storing login information in Settings and Key Chain,
	// and retrieving it as necessary. This method is only called when no login is stored (i.e. on first
	// run) or if the login retrieved was rejected by the server.
	// Typically, you would implement this method to bring up a UI that asks the user for his credentials.
 
	// By default, new Relativity Server domains will use "Data"/"Relativity" as login, unless
	// the login provider setup has been changed by the administrator.
 
	*login = @"Data";
	*password = @"Relativity";
 
	return YES; // return YES if the user provided login details, or NO if the user canceled
}

7. Implementing the RootViewController

You can now configure the root view to show the list of clients:

Open the RootViewController.m file and locate the myTable method that specifies the main data table for the whole current view. Since the default name myTable is a bit nondescript, let’s use the Refactoring>Rename option to rename it viewData.
Now add the following implementation:

- (DADataTable *)viewData {
 
	//#warning Implement this method to return the table this view will work on
	return [[DataAccess sharedInstance] clientsTable];
}

Also, let’s give the root view a more meaningful title.
By default, the root view is called “Root View”. Let’s change it to something more appropriate:
Add a string define with the title and use it in the viewDidLoad method as shown below:

#define VIEW_TITLE @"Our Clients"
 
- (void)viewDidLoad 
{
	[super viewDidLoad];
	[self setTitle:VIEW_TITLE];
...

Now you can compile the project and see the results in the iPhone simulator; it should look something like this:

8. Adding a detail view “Orders”

As next step, let’s implement the detail view that will show orders for each particular client.
First, add a new UITableViewController class with its xib file.

Make sure that your controller is based on the UITableViewController class.

The controller will take the DADataTableRow instance with the client info and then obtain and show a list of orders for the given client.

Our OrdersViewController.h will have something like the following:

@interface OrdersViewController : UITableViewController {
 
    NSArray *orderRows;
    DADataTableRow *clientRow;
}
 
@property (retain) DADataTableRow *clientRow;
@end

9. Obtaining an orders list for a given client

Now you need to hook on the moment when the client row will be set as the clientRow property and obtain the list of orders. Let’s write a custom setter method for this:

-(void)setClientRow:(DADataTableRow *)value {
 
    // set the new client row
    [value retain];
    [clientRow release];
    clientRow = value;
 
    // obtain the list of orders
    [orderRows release];
    orderRows = [[[DataAccess sharedInstance] ordersForClientID:[clientRow valueForKey:@"ClientId"]] 
retain];
 
    // reload data in the UI
    [[self tableView] reloadData];
    [self setTitle:[clientRow valueForKey:@"ClientName"]];
}

Now is a good time for adding the ordersForClientID: method inside our DataAccess class.
Its implementation is rather simple. Since the DataAccess already has the whole orders table, your task will be to extract only the orders for the given client.
Let’s use NPredicate for this:

- (NSArray *)ordersForClientID:(NSString *)clientID {
 
    NSPredicate *condition = [NSPredicate predicateWithFormat:@"Client == %@", clientID];
    return [ordersTable rowsFilteredUsingPredicate:condition];
}

Finally, let’s implement what you want to expose in the clients list.
Locate the method and implement it as shown below:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {
	// ToDo: Configure the cell. You may want to simply feed data from your table 
        // into a regular UITableViewCell
	// or provide your own customized cell imlementation to display the data in a richer format.
 
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
             cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                                            reuseIdentifier:CellIdentifier] autorelease];
	}
 
	//#warning Implement this method to customize how your data is displayed
	DADataTableRow *row = [rows objectAtIndex:indexPath.row];
	[[cell textLabel] setText:[row valueForKey:@"ClientName"]];
 
	return cell;
}

10. Implementing the data presentation for the orders view

Now let’s form an appropriate representation of the data for each customer order with minimal effort, meaning that we avoid creating custom cells for this table and just use one of predefined styles provided by Apple.

Locate and open the tableView:cellForRowAtIndexPath: method of the OrdersViewController.m file and change it like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *CellIdentifier = @"OrdersCell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                                       reuseIdentifier:CellIdentifier] autorelease];
    }
 
    // Configure the cell...
    NSDateFormatter *df = [[[NSDateFormatter alloc] init] autorelease];
    [df setDateStyle:NSDateFormatterMediumStyle];
 
    NSNumberFormatter *cf = [[NSNumberFormatter alloc] init];
    [cf setNumberStyle:NSNumberFormatterCurrencyStyle];
 
    DADataTableRow *r = [orderRows objectAtIndex:indexPath.row];
    NSDate *orderDate = [r valueForKey:@"OrderDate"];
    NSNumber *orderSum = [r valueForKey:@"OrderSum"]; 
    [[cell textLabel] setText:[df stringFromDate:orderDate]];
    [[cell detailTextLabel] setText:[cf stringFromNumber:orderSum]];
    return cell;
}

Here I should note that we used UITableViewCellStyleValue1 as the cell style, which allows us to compose the main text label with the order date at the left side and the detail label with order total at the right label of the cell. Also please note that the OrderSum field is the calculated field you have added earlier.

I propose to improve the cell view for your root view a bit and show the client’s phone as detail information. Locate the tableView:cellForRowAtIndexPath: method in the RootViewController.m file and change it to something like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {
	// ToDo: Configure the cell. You may want to simply feed data from your table
        // into a regular UITableViewCell
	// or provide your own customized cell imlementation to display the data in a richer format.
 
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle 
                                               reuseIdentifier:CellIdentifier] autorelease];
	}
 
	//#warning Implement this method to customize how your data is displayed
	DADataTableRow *row = [rows objectAtIndex:indexPath.row];
	[[cell textLabel] setText:[row valueForKey:@"ClientName"]];
        [[cell detailTextLabel] setText:[row valueForKey:@"ContactPhone"]];
        return cell;
}

Let’s compile and test our application. It should show the “Our Clients” view with a list of clients, and when you tap on a particular client it will switch to the Orders view which will show orders for the tapped client.

Looks good, doesn’t it? But there’s still one thing to improve: for clients without any orders, it shows an empty orders view. But there is an easy way to change that.

11. Final adjustments.

Let’s define whether the given client has any orders, and if it does, let’s add a disclosure button for this cell, so you will know if there is an orders view to show for the given customer or not.

To do this, let’s add another calculated field to your clientsTable as shown below:

- (void)setupData {
	...    
    [[self clientsTable] addCalculatedFieldName:@"OrderCnt" 
                                      dataType:datInteger
                                        target:self
                                      selector:@selector(calculateOrdersCountForClient:)];
	...
}
 
-(id)calculateOrdersCountForClient:(DADataTableRow *)row {
 
	NSPredicate *p = [NSPredicate predicateWithFormat:@"Client == %@", [row valueForKey:@"ClientId"]];
	NSArray *rows = [[ordersTable rows] filteredArrayUsingPredicate:p];
	return [NSNumber numberWithInt:[rows count]];
 
}

Now, you can analyze this field value and make a decision whether to draw a disclosure button to the cell in the RootViewController.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	...
    if([[row valueForKey:@"OrderCnt"] intValue] > 0)
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
    else
        cell.accessoryType = UITableViewCellAccessoryNone;
 
	return cell;
}

In conclusion, you will need to move the implementation of the loading orders view from
tableView:didSelectRowAtIndexPath:
to
tableView:accessoryButtonTappedForRowWithIndexPath:.

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
    // Pass the selected object to the new view controller.
    DADataTableRow *row = [rows objectAtIndex:indexPath.row];
    [detailViewController setClientRow:row];
 
    [self.navigationController pushViewController:detailViewController animated:YES];
}

And that’s all. Compile and run the resulting application. You should have something like the following:

In my next blog post, I’ll show you how to further improve this application and add the ability to edit and save data.

Thanks

1 response to Creating a Simple iOS Client for Relativity Server

  1. Thank you very much! Good example!