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'; |
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; |
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; |
method DataAccess.loadInitialData;begin dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),methodbegin...downloadData();...dispatch_async(dispatch_get_main_queue(),-> triggerDataReady); |
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; |
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; |
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; |
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; |
method DataAccess.OrdersByClient(client: String): NSArray;beginvar filterPredicate := NSPredicate.predicateWithFormat('Client == %@', client);result:=self.Orders.rowsFilteredUsingPredicate(filterPredicate);end; |
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; |
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; |
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; |
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! ;)