Data Abstract Sample for Oxygene for Cocoa

Hi guys,

Recently I’ve spotted a question about Nougat samples in the support thread. Unfortunately, we didn’t offer any sample applications at that time, so I’ve tried to quickly compose one.

We’ve already proposed some New Project templates for Data Abstract for Cocoa though:

Using these templates, and after going through several steps in the wizard, we can get an almost complete data-aware application (we just need to review and adjust the code in several places marked with a #warning directive).

So I’ve started with this New Project wizard and created a client for the PCTrade sample domain exposed by Relatvitiy server.

In this sample, I want to show several things:

  • Working with Relativity server.
  • Downloading data at the client side in an asynchronous way.
  • Using Cocoa NotificationCenter.
  • Master-Detail navigation in the data.
  • Using Calculated fields for the DADataTable.
  • Conditional formatting of the table view cells.
  • The application should be universal and support both iPhone and iPad.

At the same time, I’ve tried to keep things as simple as possible to make it easy to understand common ideas.

So let’s start:

Working with Relativity domains

Almost all implementation required for connecting to the Relativity server, including autentication at the Relativity side, is done by the template and new project wizard. Most important places in the code are highlighted with the #warning directive. For example, in the *DataAccess.pas *file you can see the following place:

{$WARNING Define your server address and Relativity domain name, below.}const SERVER_URL ='http://192.168.0.22:7099/bin';const SERVICE_NAME ='DataService';const RELATIVITY_DOMAIN ='PCTrade Sample';const DOMAIN_SCHEMA ='PCTrade';
Here you can adjust the URL of your [Relativity server](http://wiki.remobjects.com/wiki/Relativity_Server "Relativity") (my server uses http://192.168.0.22:7099/bin). These constants will also be used for configuring the [RemoteDataAdapter](http://wiki.remobjects.com/wiki/DARemoteDataAdapter_Class "DARemoteDataAdapter") instance.

Another place that requires our attention is in the AppDelegate.pas and holds the passwords to log in to the Relativity domain:

method AppDelegate.needLogin(var aLogin: String) password(var aPassword: String): Boolean;begin// 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 retrieved login was rejected by the server.// Typically, you would implement this method to bring up the 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. aLogin :='Data'; aPassword :='Relativity';   result:=true;// return YES if the user provided login details, or NO if the user canceledend;
Of course, in the real application, you can implement a popup window here with to enter login and password, or even get credentials from the application preferences, but I’m trying to keep it simple here.

Downloading data

Again, like in the point above, almost all implementation for downloading data from the server was generated automatically by the new project wizard.

You may notice that all the tables we selected in one of the last wizard step, were created with the appropriate properties in DataAccess.pas and code has been generated to fill them.

method DataAccess.downloadData;begin// ToDo: Add code to download data from server// You will need to define fields and, if necessary, properties for your tables.   var tableNames :=new NSMutableArray; tableNames.addObject('Clients'); tableNames.addObject('OrderDetails'); tableNames.addObject('Orders');   var tables := fRDA.getDataTables(tableNames);   Clients := tables['Clients']; OrderDetails := tables['OrderDetails']; Orders := tables['Orders'];end;
It is important to note here that the given *downloadData* method is called asynchronously (in a separate thread), so we get a responsive and non-frozen UI.
method DataAccess.loadInitialData;begin dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),methodbegin...downloadData();...dispatch_async(dispatch_get_main_queue(),-> triggerDataReady);
## Using notifications

Since we are downloading data in a separate thread, we need to notify our main UI thread with the results.
And here the Cocoa Notification Center comes in to help us.
In the code above you may have notice that after downloading the data, we perform a triggerDataReady call at the main thread. This method just posts a notification to the Notification Center that the job is done.

method DataAccess.triggerDataReady;begin dataReady :=true; NSNotificationCenter.defaultCenter.postNotificationName(NOTIFICATION_DATA_READY) object(self); NSLog('data is ready.');   // ToDo: Add any processing you want to do once the data is fully readyend;
Thus, if our controller needs to be informed about loading data results, we need to subscribe it to this notification. You can find the code in the *ClientsViewController.pas* file.
  method ClientsViewController.viewDidLoad;begininherited viewDidLoad;// Do any additional setup after loading the view, typically from a nib...NSNotificationCenter.defaultCenter.addObserver(self) &selector(selector(dataReady:)) name(DataAccess.NOTIFICATION_DATA_READY) object(DataAccess.sharedInstance);...end;   method ClientsViewController.dataReady(notification: NSNotification);begin tableView.reloadData();end;
That means that when our *ClientsViewController* receives the *DataReady* notification, the *dataReady:* method must be invoked, which then refreshes the content of the table view.

Master-Detail navigation

This feature is part of the New Project template. By default, it generates already configured Master and Detail view controllers and the corresponding storyboards files with proper transitions between views inside.

Concrete implementation depends on device type.
For iPhone/iPod devices with their relatively small screens, when user selects a certain cell, the proper transition will be invoked and we can hook on it and set the info for the detail controller.

method ClientsViewController.prepareForSegue(segue: UIStoryboardSegue) sender(sender: id);beginif segue.identifier.isEqualToString('showOrders')thenbeginvar ip := tableView.indexPathForCell(sender);var tableRow :=self.dataTable.rowAtIndex(ip.row); segue.destinationViewController.setDetailItem(tableRow);end;end;
For the iPad device, the master and detail view are visible at the same time – no transition required. So we can just hook at the *didSelectRow* event and provide the details data for the detail controller as shown below:
method ClientsViewController.tableView(tableView: UITableView) didSelectRowAtIndexPath(indexPath: NSIndexPath);beginif UIDevice.currentDevice.userInterfaceIdiom= UIUserInterfaceIdiom.UIUserInterfaceIdiomPadthenbeginvar tableRow :=self.dataTable.rowAtIndex(indexPath.row); OrdersViewController.detailItem:= tableRow;end;end;
About the *details* data: Since all necessary data is already present at the client-side, we are performing just a local data filtering using *NSPredicates*:
method DataAccess.OrdersByClient(client: String): NSArray;beginvar filterPredicate := NSPredicate.predicateWithFormat('Client == %@', client);result:=self.Orders.rowsFilteredUsingPredicate(filterPredicate);end;
## Using Calculated fields for the DADataTable

As you might know, we can easily extend any existing DADataTable with our custom lookup and calculated fields which could help us get various related information. For this sample I need to know the amount of orders for each of our clients and also want to know the total for each client order (which can be calculated by summing the OrderDetails rows).

There is the special setupData method in the DataAccess class. It will be called while loading the initial data, so I can be sure that my tables will be extended in a proper way.

Declaring calculated fields is easy, we just need to specify the field name, its type and the target and selector for the method that will be used for calculation.

method DataAccess.setupData;begin// Use this method to implement any setup that is needed on your data tables, such as// creating lookup or calculated fields, etc.self.Clients.addCalculatedFieldName('OrdersCnt') dataType(__enum_DADataType.datInteger) target(self) &selector(selector(calculateOrdersCntForClientRow:));   self.Orders.addCalculatedFieldName('OrderSum') dataType(__enum_DADataType.datCurrency) target(self) &selector(selector(calculateSumForOrderRow:));end;
As a rule, calculation methods take a row instance as the parameter and return an object (id) as the result of the calculation. So here is the sample of our calculation method:
method DataAccess.calculateSumForOrderRow(row: DADataTableRow): id;begin// Get the Order ID value from the row var orderID: Integer := row.valueForKey('OrderId').integerValue;// Prepare filter to obtain details on given ordervar condition: NSPredicate := NSPredicate.predicateWithFormat('Order == %d', orderID);// Get the order totalvar orderSum: Double :=self.OrderDetails.rowsFilteredUsingPredicate(condition).valueForKeyPath('@sum.Total').doubleValue();   // Wrap it into an id object and returnexit NSNumber.numberWithDouble(orderSum);end;
## Conditional cell formatting

It has become common practice to give a slightly different design to each cell depending on the data it represents. For example, the developer can play with font, size, color, etc. All this makes the data more obvious and easy to perceive.

We will use a similar approach for our application. We already have the method tableView() cellForRowAtIndexPath(): UITableViewCell which composes the cell for any table row we want to show.

method ClientsViewController.tableView(tableView: UITableView) cellForRowAtIndexPath(indexPath: NSIndexPath): UITableViewCell;begin   var CellIdentifier := "Cell";// Try to obtain cached cell   result:= tableView.dequeueReusableCellWithIdentifier(CellIdentifier);     // This is required only if you are targetting iOS 5.1 or lower   ifnotassigned(result)then// If there is no such cell cached, then just create a new one     result:=new UITableViewCell withStyle(UITableViewCellStyle.UITableViewCellStyleValue1)                                    reuseIdentifier(CellIdentifier);      // Note that we used UITableViewCellStyleValue1 as the UITableViewCellStyle// which makes the cell with the value aligned to the left side and the detail value aligned to the right side of the cell   var tableRow :=self.dataTable.rowAtIndex(indexPath.row);   result.textLabel.text:= tableRow['ClientName'];   // Get the count of orders (as you remember, OrdersCnt is the calculated field)   var ordersCnt: Integer := tableRow['OrdersCnt']:integerValue();   // And depending on the orders count we can use different colors and different icons for the given cells   case ordersCnt of     0:begin          result.detailTextLabel.textColor:=  UIColor.blueColor();         result.imageView.image:= UIImage.imageNamed('1');         result.detailTextLabel.text:='No orders';      end;     1:begin       result.detailTextLabel.textColor:= UIColor.orangeColor();       result.imageView.image:= UIImage.imageNamed('2');       result.detailTextLabel.text:= NSString.stringWithFormat('%d order', ordersCnt);     end;     2:begin       result.detailTextLabel.textColor:= UIColor.magentaColor();       result.imageView.image:= UIImage.imageNamed('3');       result.detailTextLabel.text:= NSString.stringWithFormat('%d orders', ordersCnt);     end;     elsebegin        result.detailTextLabel.textColor:=  UIColor.redColor();        result.imageView.image:= UIImage.imageNamed('4');        result.detailTextLabel.text:= NSString.stringWithFormat('%d orders', ordersCnt);     end;   end;    end;
As a result of our efforts above, we now have the following application:

And finally, as a small bonus, this is how it looks in iOS7 ;)

You can get sources of the sample download here

Thanks for your attention! ;)