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.
Now you can compile the application, after which you will see a set of warnings:
- 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" |
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 |
#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]; } |
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]]; } |
- 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]; } |
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]; } |
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"; returnYES; // return YES if the user provided login details, or NO if the user canceled} |
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 onreturn[[DataAccess sharedInstance] clientsTable]; } |
#define VIEW_TITLE @"Our Clients" -(void)viewDidLoad {[super viewDidLoad]; [self setTitle:VIEW_TITLE]; ... |
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.
Our OrdersViewController.h will have something like the following:
@interface OrdersViewController : UITableViewController { NSArray*orderRows; DADataTableRow *clientRow; } @property(retain) DADataTableRow *clientRow; @end |
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"]]; } |
-(NSArray*)ordersForClientID:(NSString*)clientID { NSPredicate*condition =[NSPredicate predicateWithFormat:@"Client == %@", clientID]; return[ordersTable rowsFilteredUsingPredicate:condition]; } |
// 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. staticNSString*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; } |
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 { staticNSString*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; } |
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. staticNSString*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; } |
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.
- 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]]; } |
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { ... if([[row valueForKey:@"OrderCnt"] intValue] > 0) cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; else cell.accessoryType = UITableViewCellAccessoryNone; return cell; } |
-(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]; } |
Thanks