Shahzad Bhatti Welcome to my ramblings and rants!

April 26, 2012

How to survive in today’s work environments and businesses

Filed under: Business — admin @ 10:39 pm

In modern work environments and businesses, it is crucial to expedite learning cycle and create a knowledge workplace. In order to shorten the learning cycle, you can apply the scientific approach, which comprises of three stages, i.e., making a hypothesis, testing the hypothesis and validating test results. Generally, the test results leads to another hypothesis, and another cycle of experiment, validation and then publishing results. At the end of each cycle, you learn something knew, thus the shorter your cycle the faster you learn. Following are few variations of this approach used in manufacturing, research and development:

OODA

John Boyd revolutionized military aviation strategy by designing lightweight fighter planes that were based on a shorter loop of

  • Observe
  • Orient
  • Decide
  • Act

John showed that light-weight planes that provided quick feedback to pilots proved better in dogfights. It showed that speed of loop beats quality of iteration.

Theory of Constraint

The theyory of constraint creates flow of an activity and finds all constraints from end to end and then tries to remove biggest constraint. It then repeats the process and identifies next biggest constraint and solves that constraint.

Lean Manufacturing

Lean manufacturing is based on Toyota production system that emphasizes waste elimination and creating value for customers. Lean production system stresses reducing activity time from start to finish and continuous improvement or Kaizen.

Lean Software Development

Lean software development applies lessons of Toyota production systems to software development by eliminating waste, reducing in-progress work (inventory), and amplifying learning. It speeds up learning process by short cycle of iterative development.

Lean Startup

The startups generally start with visions or hypothesis that are unproven. Lean startups borrow ideas from Toyota production systems by creating rapid prototypes that test market assumptions and uses customer feedback evolve the product. Just like the scientific approach, it uses a cycle of product-idea or hypothesis, product-development or preparing an experiment, releasing the product, where startups collects data about value to users. The test results help startups adjust the product and another round of tests are followed. The Lean Startups emphasized validated learning that you gain from running actual experiment than just guess work.

Spiral Methodology

Spiral methodology is a software development process that uses iterative development and encourages prototyping and experimenting. Each iteration starts with objectives, constraints, and alternatives, which are then evaluated, developed and then validated.

Scrum Methodology

Scrum is an iterative and incremental agile software methodology for project management. It uses short sprints for software development, where each sprint starts with planning meeting that defines the user stories or features to be delivered, followed by development and release. At the end of sprint, the team holds Sprint review meeting and retrospective to identify impediments so that team can improve the process. The whole process is repeated with another round of short sprints.

Other Agile Methodologies

There are a number of other agile methodologies that also founded on short iterations and incremental development such as Agile Unified Process, Crystal Clear, XP, Feature driven development. These methodologies encourage transparent, collaborative and open work environments, which provide foundation for adoptive and knowledge workplaces.

Test-Driven Development

Test-driven development is a software development process uses a short cycle of development, where developer writes a failing test case for desired functionality, then implements functionality to pass the test and finally refactors the new code. It eliminates the waste by focusing on business functionality that is required and helps build design incrementally. Each cycle of TDD is very short and provides rapid feedback to developer if the code is working.

Integrated Development Environment (IDE)

Modern IDEs are built to shorten development cycle by providing rapid feedback to developer such as syntax warnings, errors and integration to testing, debugging, static analysis, deploying and other tools. The productivity of developer increases when the cycle from edit, build to test, debug or deploy is short.

One-on-One vs Annual Reviews

Unfortunately Annual Reviews are still annual rituals in most companies that provide feedback once a year. Instead weekly one-on-one provide shorter feedback and is more effective.

Opinions vs Data

All of us have plenty of opinions which are nothing more than guesses. Modern development methodologies such as Lean software development or Lean startup encourages data-driven approach by executing short experiments and learning from the experiments.

Summary

We live in rapidly changing knowledge economy. We need to learn how to be nimble and to create a culture that speeds up learning process by performing short experiments. Instead of working in vacuum, we need to validate our assumptions and guesses with actual experiments and take data-driven approach to test our hypothesis. This approach is more bottom-up approach but it doesn’t mean that we don’t have a vision. It just means, we are continuously learning, improving and adopting as our environment changes.

March 10, 2012

Building custom split view controller and tab view controller with horizontal/vertical orientations for iPhone and iPad

Filed under: iPhone development — admin @ 10:49 am

Over the last few years, I have been developing iPhone and iPad applications and one of recurring problem is managing how to present information in flexible way that works on both iPhone and iPad. The iOS platform uses UITabBarController to organize controllers, however it also comes with a number of limitations such as it only show five tabs on iPhone and though you can add more tabs but you have to go through “More” tab to access them. Also, it always show the tabs on the bottom and you cannot change their position.

I would show how you can develop a customized tab controller that works on both iPad and iPhone and supports multiple orientations.

Custom Split View Controller

The first thing I needed was to split screen between tab-bars and the main view and though iPad platform supports split view controller (UISplitViewController), however, there are several limitations of the builtin UISplitViewController class such as it only works on iPad in landscape mode and it does not work in iPhone. Here is an example of customized split view controller:

 #import "MasterDetailSplitController.h"
 #import "MenuViewController.h"
 #import "MenuCellView.h"
 
 
 @implementation MasterDetailSplitController
 
 @synthesize menuViewController = _menuViewController;
 @synthesize detailsViewController = _detailsViewController;
 @synthesize menuView = _menuView;
 @synthesize detailsView = _detailsView;
 
 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
     if((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
     }
     return self;
 }
 
 - (void)dealloc {
     [_menuViewController release];
     [_detailsViewController release];
     [_menuView release];
     [_detailsView release];
     [super dealloc];
 }
 
 
 
 #pragma mark - View lifecycle
 
 - (void)viewDidLoad {
     [super viewDidLoad];
     isPortrait = YES;
     
     self.menuViewController = [[[MenuViewController alloc] initWithSplit:self] autorelease];
     self.menuViewController.view.frame = CGRectMake(0, 0, self.menuView.frame.size.width, self.menuView.frame.size.height);
     [self.menuView addSubview:self.menuViewController.view];
     self.menuView.transform = CGAffineTransformMakeRotation(-M_PI * 0.5);
     self.menuView.autoresizesSubviews = YES;
     self.menuView.backgroundColor = [UIColor grayColor];
     [self layoutSubviews];
 
 }
 
 
 - (void)viewDidUnload {
     [super viewDidUnload];
     self.menuViewController = nil;
     self.detailsViewController = nil;
     self.detailsView = nil;
 }
 
 
 - (void) pushToDetailController:(UIViewController *)controller {
     if (self.detailsViewController == controller) {
         return;
     }
     if (self.detailsViewController != nil && [self.detailsViewController isKindOfClass:[UINavigationController class]]) {
         UINavigationController *navCtr = (UINavigationController *)self.detailsViewController;
         if ([navCtr.viewControllers containsObject:controller]) {
             return;
         }
     }
     for (UIView *v in self.detailsView.subviews) {
         [v removeFromSuperview];
     }
     if (![controller isKindOfClass:[UINavigationController class]]) {
         UINavigationController *navCtr = [[[UINavigationController alloc] initWithRootViewController:controller] autorelease];
         [navCtr setDelegate:self];
         controller = navCtr;
     }
 
     [self layoutSubviews];
 
     controller.view.frame = CGRectMake(0, 0, self.detailsView.frame.size.width, self.detailsView.frame.size.height);
     controller.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth
     | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin
     | UIViewAutoresizingFlexibleRightMargin  | UIViewAutoresizingFlexibleLeftMargin;
     
     self.detailsView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;    
     self.detailsViewController = controller;
     [self.detailsView addSubview:controller.view];
 
 }
 
 
 
 
 #pragma mark -
 #pragma mark STANDARD METHODS
 
 - (void) viewWillAppear:(BOOL)animated {
     [super viewWillAppear:animated];
     
     [self.menuViewController viewWillAppear:animated];
     [self.detailsViewController viewWillAppear:animated];
 }
 
 - (void) viewDidAppear:(BOOL)animated {
     [super viewDidAppear:animated];
     [self.menuViewController viewDidAppear:animated];
     [self.detailsViewController viewDidAppear:animated];    
 }
 
 - (void)viewWillDisappear:(BOOL)animated {
     [super viewWillDisappear:animated];
     [self.menuViewController viewWillDisappear:animated];
     [self.detailsViewController viewWillDisappear:animated];
 }
 
 - (void)viewDidDisappear:(BOOL)animated {
     [super viewDidDisappear:animated];
     [self.menuViewController viewDidDisappear:animated];
     [self.detailsViewController viewDidDisappear:animated];
 }
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
     return YES; 
 }
 
 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     isPortrait = toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown;
     if (isPortrait) {
         self.menuView.transform = CGAffineTransformMakeRotation(-M_PI * 0.5);
     } else {        
         self.menuView.transform = CGAffineTransformIdentity;
     }
     [self.menuViewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
     [self.detailsViewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
 }
 
 
 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
     isPortrait = fromInterfaceOrientation != UIInterfaceOrientationPortrait && fromInterfaceOrientation != UIInterfaceOrientationPortraitUpsideDown;
     [self layoutSubviews];
     [self.menuViewController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
     [self.detailsViewController didRotateFromInterfaceOrientation:fromInterfaceOrientation];
 }
 
 - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     [self.menuViewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
     [self.detailsViewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
 }
 
 - (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     [self.menuViewController willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
     [self.detailsViewController willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
 }
 
 - (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
     [self.menuViewController didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation];
     [self.detailsViewController didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation];
 }
 
 - (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration {
     [self.menuViewController willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration];
     [self.detailsViewController willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration];
 }
 
 
 #pragma mark -
 #pragma mark Helpers
 - (CGSize) sizeRotated {    
     UIScreen *screen = [UIScreen mainScreen];
     CGRect bounds = screen.bounds;
     CGRect appFrame = screen.applicationFrame;
     CGSize size = bounds.size;
     
     float statusBarHeight = MAX((bounds.size.width - appFrame.size.width), (bounds.size.height - appFrame.size.height));
     
     if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) 
     {
         // we're going to landscape, which means we gotta swap them
         size.width = bounds.size.height;
         size.height = bounds.size.width;
     }
     
     size.height = size.height - statusBarHeight -self.tabBarController.tabBar.frame.size.height;
     return size;
 }
 
 - (void) layoutSubviews {    
     CGSize size = [self sizeRotated];
     if (isPortrait) {
         self.detailsView.frame = CGRectMake(0, 0, size.width, size.height-kMENU_CELL_HEIGHT);
         self.menuView.frame = CGRectMake(0.0, size.height-kMENU_CELL_HEIGHT, size.width, kMENU_CELL_HEIGHT);
     } else {        
         self.menuView.frame = CGRectMake(0.0, 0.0, kMENU_CELL_WIDTH, size.height);
         self.detailsView.frame = CGRectMake(kMENU_CELL_WIDTH, 0, size.width-kMENU_CELL_WIDTH, size.height);
     }
 }
 
 
 
 - (void) loadView {
     CGSize size = [self sizeRotated];
     
     UIView *view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] autorelease];
     view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
     self.view = view;
     
     self.menuView = [[[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, kMENU_CELL_WIDTH, size.height)] autorelease];
     self.menuView.autoresizesSubviews = YES;
     self.menuView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleHeight;
     self.menuView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:1.000];
     self.menuView.contentMode = UIViewContentModeScaleToFill;
     self.detailsView = [[[UIView alloc] initWithFrame:CGRectMake(kMENU_CELL_WIDTH, 0, size.width-kMENU_CELL_WIDTH, size.height)] autorelease];
     self.detailsView.autoresizesSubviews = YES;
     self.detailsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
     self.detailsView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:1.000];
     self.detailsView.contentMode = UIViewContentModeScaleToFill;
     
     [self.view addSubview:self.menuView];
     [self.view addSubview:self.detailsView];
 }
 
 
 
 
 @end
 

You can customize offset of split controller by setting values for kMENU_CELL_WIDTH and kMENU_CELL_HEIGHT. Note that split view controller transforms the menu-view controller and rotates it 90″ when orientation changes to portrait so that table can be scrolled horizontally.

Building Menu Controller, that can scroll vertically and horizontally

Next, you need a UITableViewController, which can scroll horizontal and vertically. You can use transform to change the view such as:

 #import "MenuViewController.h"
 #import "SampleTableController.h"
 #import "Configuration.h"
 #import "MenuCellView.h"
 #import "MenuInfo.h"
 #import "CustomNavigationController.h"
 
 
 
 @implementation MenuViewController
 @synthesize menuItems = _menuItems;
 @synthesize masterDetailSplitController = _masterDetailSplitController;
 
 
 - (id)initWithSplit:(MasterDetailSplitController*)split {
     self = [super initWithStyle:UITableViewStylePlain];
     if (self) {        
         self.masterDetailSplitController = split;
         self.tableView.separatorColor = [UIColor clearColor];
         self.menuItems = [[[NSMutableArray alloc] initWithCapacity:10] autorelease];
     }
     return self;
 }
 
 
 -(void)refresh {
     MenuInfo *menu = nil;
     [self.menuItems removeAllObjects];
     CustomNavigationController *settingsCtr = [[[CustomNavigationController alloc] initWithStyle:UITableViewStylePlain] autorelease];
     menu = [[[MenuInfo alloc] initWithLabel:@"Settings" andOnImage:@"onSettings.png" andOffImage:@"offSettings.png" andController:settingsCtr] autorelease];
     [self.menuItems addObject:menu];
     
     NSArray *orderList = [[Configuration sharedConfiguration] getNavigationOrder];
     for (NSString *order in orderList) {
         MenuKind kind = (MenuKind) [order intValue];
         SampleTableController *ctr = [[[SampleTableController alloc] initWithStyle:UITableViewStylePlain] autorelease];
         switch (kind) {
             case ONE:
                 ctr.heading = @"One";
                 menu = [[[MenuInfo alloc] initWithLabel:@"One" andOnImage:@"onDown.png" andOffImage:@"offDown.png" andController:ctr] autorelease];
                 break;
             case TWO:
                 ctr.heading = @"Two";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Two" andOnImage:@"onDownLeft.png" andOffImage:@"offDownLeft.png" andController:ctr] autorelease];
                 break;
             case THREE:
                 ctr.heading = @"Three";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Three" andOnImage:@"onDownRight.png" andOffImage:@"offDownRight.png" andController:ctr] autorelease];
                 break;
             case FOUR:
                 ctr.heading = @"Four";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Four" andOnImage:@"onLeft.png" andOffImage:@"offLeft.png" andController:ctr] autorelease];
                 break;
             case FIVE:
                 ctr.heading = @"Five";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Five" andOnImage:@"onNext.png" andOffImage:@"offNext.png" andController:ctr] autorelease];
                 break;
             case SIX:
                 ctr.heading = @"Six";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Six" andOnImage:@"onPrevious.png" andOffImage:@"offPrevious.png" andController:ctr] autorelease];
                 break;
             case SEVEN:
                 ctr.heading = @"Seven";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Seven" andOnImage:@"onRight.png" andOffImage:@"offRight.png" andController:ctr] autorelease];
                 break;
             case EIGHT:
                 ctr.heading = @"Eight";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Eight" andOnImage:@"onUp.png" andOffImage:@"offUp.png" andController:ctr] autorelease];
                 break;
             case NINE:
                 ctr.heading = @"Nine";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Nine" andOnImage:@"onUpLeft.png" andOffImage:@"offUpLeft.png" andController:ctr] autorelease];
                 break;
             case TEN:
                 ctr.heading = @"Ten";
                 menu = [[[MenuInfo alloc] initWithLabel:@"Ten" andOnImage:@"onUpRight.png" andOffImage:@"offUpRight.png" andController:ctr] autorelease];
                 break;
         }
         [self.menuItems addObject:menu];
     }
     selected = 0;
     [self.tableView reloadData];
 }
 
 - (UIViewController *) selectedController {
     MenuInfo *info = [self.menuItems objectAtIndex:selected];
     return info.controller;
 }
 
 
 - (void)dealloc
 {
     [_masterDetailSplitController release];
     [_menuItems release];
     [super dealloc];
 }
 
 
                                
 #pragma mark - View lifecycle
 
 - (void)viewDidLoad
 {
     [super viewDidLoad];
     isPortrait = YES;
     UIView *bgView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
     bgView.transform = CGAffineTransformMakeRotation(M_PI * 0.5);
     self.tableView.backgroundView = bgView;
     self.view.backgroundColor = [UIColor blackColor];
 }
 
 - (void)viewWillAppear:(BOOL)animated
 {
     [super viewWillAppear:animated];
     self.tableView.showsVerticalScrollIndicator = NO;
     self.tableView.showsHorizontalScrollIndicator = NO;        
     self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
     self.tableView.separatorColor = [UIColor clearColor];
     
     [self refresh];
     [self.masterDetailSplitController pushToDetailController:[self selectedController]];    
 
 }
 
 
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;
 }
 
 -(NSArray *)allControllers {
     NSMutableArray *list = [[[NSMutableArray alloc] initWithCapacity:10] autorelease];
     for (MenuInfo *info in self.menuItems) {
         [list addObject:info.controller];
     }
     return list;
 }
 
 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     isPortrait = toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown;
 
     for (UIViewController *ctr in [self allControllers]) {
         [ctr willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
     }
     [self.tableView reloadData];
 }
 
 
 
 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
     for (UIViewController *ctr in [self allControllers]) {
         [ctr didRotateFromInterfaceOrientation:fromInterfaceOrientation];
     }
 }
 
 - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     for (UIViewController *ctr in [self allControllers]) {
         [ctr willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
     }
 }
 
 - (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
     for (UIViewController *ctr in [self allControllers]) {
         [ctr willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
     }
 }
 
 - (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
     for (UIViewController *ctr in [self allControllers]) {
         [ctr didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation];
     }
 }
 
 - (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration {
     for (UIViewController *ctr in [self allControllers]) {
         [ctr willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration];
     }
 }
 
 
 #pragma mark - Table view data source
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView
 {
     return 1;
 }
 
 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
 {
     return [self.menuItems count];
 }
 
 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
 {
     static NSString *CellIdentifier = @"MenuCell";
     MenuCellView *cell = (MenuCellView *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
     if (cell == nil) {
         cell = [[[MenuCellView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
     }
     MenuInfo *menu = [self.menuItems objectAtIndex:indexPath.row];
     NSString *imageName = selected == indexPath.row ? menu.onImage : menu.offImage;
     cell.imageView.image = [UIImage imageNamed:imageName];
     
     if (isPortrait) {
         cell.transform = CGAffineTransformMakeRotation(M_PI * 0.5);
     } else {
         cell.transform = CGAffineTransformIdentity;        
     }
     
     return cell;
 }
 
 /**
  * Displays the specified controller view
  */
 -(void) displayControllerView:(UIViewController*)controller {
     if (controller != nil) {
         [self.masterDetailSplitController pushToDetailController:controller];
     }
     [self.tableView reloadData];
 }
 
 
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     selected = indexPath.row;
     
     UIViewController *controller = [self selectedController];
  
     [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
     [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
 
     [self displayControllerView:controller];
 }
 
 
 
 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
     if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
         return kMENU_CELL_WIDTH * 3;
     } else {
         return kMENU_CELL_WIDTH;
     }
 }
 
 
 
 @end
 

The height of cells for iPad is stretched so that you can test scrolling across the screen. The cells are also rotated 90″ when orientation is in portrait mode so that they are displayed properly on the horizontal table. Also, if you have a background image, then you will need to rotate it as well.

Customizing order of tabs

You can also allow users to change the order by storing order in a configuration, here is an example of controller that allows reordering:

 #import "CustomNavigationController.h"
 #import "MenuViewController.h"
 #import "Configuration.h"
 #import "AppDelegate.h"
 
 @implementation CustomNavigationController
 
 @synthesize controllers;
 
 static NSArray *kControllerNames;
 
 + (void) initialize {
     kControllerNames = [[NSArray arrayWithObjects:@"One", @"Two", @"Three", @"Four", @"Five", @"Six", @"Seven", @"Eight", @"Nine", @"Ten", nil] retain];    
 }
 
 
 
 - (id)initWithStyle:(UITableViewStyle)style
 {
     self = [super initWithStyle:style];
     if (self) {
     }
     return self;
 }
 
 
 
 #pragma mark - View lifecycle
 
 - (void)viewDidLoad
 {
     [super viewDidLoad];
     controllers = [[NSMutableArray alloc] initWithCapacity:10];
     [super setEditing:YES animated:YES];
     [self.tableView setEditing:YES animated:YES];
     self.navigationItem.title = @"Navigation";
     self.tableView.backgroundColor = [UIColor clearColor];
     [self.tableView setIndicatorStyle:UIScrollViewIndicatorStyleWhite];
     self.tableView.separatorColor = [UIColor blackColor];
 }
 
 
 -(void)viewWillAppear:(BOOL)animated {
     [super viewWillAppear:animated];
 
     [controllers removeAllObjects];
     NSArray *list = [[Configuration sharedConfiguration] getNavigationOrder];
     for (NSString *ctr in list) {
         [controllers addObject:[NSNumber numberWithInt:[ctr intValue]]];
     }
     
     [self.tableView reloadData];
 }
 
 
 - (void)viewDidUnload
 {
     [super viewDidUnload];
     self.controllers = nil;
 }
 
 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
 {
     return YES;
 }
 
 #pragma mark - Table view data source
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
 {
     return 1;
 }
 
 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
 {
     return [controllers count];
 }
 
 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
 {
     static NSString *CellIdentifier = @"CustomNavigationCellView";
     
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
     if (cell == nil) {
         cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
     }
     
 
     NSNumber *num = [controllers objectAtIndex:indexPath.row];
     cell.textLabel.text = [kControllerNames objectAtIndex:[num intValue]];
 
     return cell;
 }
 
 
 
 #pragma mark - Table view delegate
 
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 {
 
 }
 
 
 
 - (UITableViewCellEditingStyle)tableView:(UITableView *)aTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
     return UITableViewCellEditingStyleNone;
 }
 
 
 - (void)tableView:(UITableView *)aTableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
     [self.tableView reloadData];    
 }
 
 
 #pragma mark Row reordering
 - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
     return YES;
 }
 
 - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
       toIndexPath:(NSIndexPath *)toIndexPath {
     NSNumber *item = [[controllers objectAtIndex:fromIndexPath.row] retain];
     [controllers removeObject:item];
     [controllers insertObject:item atIndex:toIndexPath.row];
     [item release];
 
     NSMutableArray *list = [[[NSMutableArray alloc] initWithCapacity:10] autorelease];
     for (NSNumber *num in controllers) {
         [list addObject:[num stringValue]];
     }
     [[Configuration sharedConfiguration] setNavigationOrder:list];
 
     AppDelegate *delegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
     [delegate.splitViewController.menuViewController refresh];
 }
 
 
 
 - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath {
     return NO;
 }
 
 -(void)dealloc {
     [controllers release];
     [super dealloc];
 }
 @end
 

Note that we are storing the order of tabs in the configuration and saving it when user moves the navigation items.

Screen shots

iPhone Portrait

iPhone Landscape

iPad Portrait

iPad Landscape

(Note that iPad images are stretched to show that they are horizontal and vertical scrollable)

Download the code

I am not showing all code here but the full code is available from https://github.com/bhatti/SplitControllerAndCustomNavigation. I hope you find it useful.


November 16, 2011

Using self-signed certificates and resolving SSL version with iOS5 SDK

Filed under: iPhone development — admin @ 7:32 am

Using Self-signed certificates with iOS5

There have been a few changes to SSL in iOS5 SDK, which caused some snafus to our iPhone app. We have been using ASIHTTPRequest library for accessing our web services that require SSL and we use self-signed certificates in test environment. I noticed that the iPhone app wasn’t able to connect to the server after upgrading to iOS5 SDK. The original code in ASIHTTPRequest.m that accepted self-signed certificates looked like:

  NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: 
                                       [NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates,
                                       [NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot,
                                       [NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain,
                                       kCFNull,kCFStreamSSLPeerName, nil];
 
   CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, (CFTypeRef)sslProperties);
 

However, the iOS5 SDK deprecated kCFStreamPropertySSLPeerCertificates, kCFStreamSSLAllowsExpiredCertificates, kCFStreamSSLAllowsExpiredRoots, kCFStreamSSLAllowsAnyRoot and requires using kCFStreamSSLValidatesCertificateChain to disable certificates, e.g.:

 
   CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, 
         [NSMutableDictionary dictionaryWithObject:(NSString *)kCFBooleanFalse forKey:(NSString *)kCFStreamSSLValidatesCertificateChain]);
   [[self readStream] setProperty:[NSDictionary dictionaryWithObjectsAndKeys: 
         (id)kCFBooleanFalse, kCFStreamSSLValidatesCertificateChain, nil] forKey:(NSString *)kCFStreamPropertySSLSettings];
 
 

Here is a complete example if you need to skip validation without using ASIHTTPRequest:

 - (void)start {
     CFStringRef bodyString = CFSTR("xml....");
     CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, bodyString, kCFStringEncodingUTF8, 0);
 
     CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("POST"), (CFURLRef) [NSURL URLWithString:@"https://optionshouse.com:443/m?"], kCFHTTPVersion1_1);
     CFHTTPMessageSetBody(request, bodyData);
 
     CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(NULL, request);
 
     self.inputStream = (NSInputStream *) readStream;
 
     [self.inputStream setProperty:[NSDictionary dictionaryWithObjectsAndKeys:(id) kCFBooleanFalse,    kCFStreamSSLValidatesCertificateChain, nil ] forKey:(NSString *) kCFStreamPropertySSLSettings];
 
     [self.inputStream setDelegate:self];
     [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
     [self.inputStream open];
 
     CFRelease(readStream);
     CFRelease(request);
 }
 
 - (void)stop {
     [self.inputStream setDelegate:nil];
     [self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
     [self.inputStream close];
     self.inputStream = nil;
 }
 
 - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
     switch (eventCode) {
         case NSStreamEventOpenCompleted: {
             NSLog(@"open");
         } break;
         case NSStreamEventHasBytesAvailable: {
             NSInteger   bytesRead;
             uint8_t     junk[1024];
 
             NSLog(@"has bytes");
             bytesRead = [self.inputStream read:junk maxLength:sizeof(junk)];
             if (bytesRead == 0) {
                 NSLog(@"read end");
                 [self stop];
             } else if (bytesRead < 0) {
                 NSLog(@"read error");
                 [self stop];
             } else {
                 NSString *string = [[[NSString alloc] initWithBytes:junk
                                                         length:bytesRead
                                                       encoding:NSUTF8StringEncoding] autorelease];
                 NSLog(@"Read %@", string);
             }
         } break;
         case NSStreamEventErrorOccurred: {
             NSError * error = [self.inputStream streamError];
             NSLog(@"error %@ / %zd", [error domain], (ssize_t) [error code]);
             [self stop];
         } break;
         case NSStreamEventEndEncountered: {
             NSLog(@"end");
             [self stop];
         } break;
         default: {
             assert(NO);
             [self stop];
         } break;
     }
 }
 
 

Using SSL with old F5 load balancers

Another issue I found with iOS5 was that despite using valid certificate in production environment, the SSL was still not working. Based on Apple's documentation, it turned out that the default version for SSL has been changed to TLS 1.0 and our production environment used F5 load balancer, which required SSLv3. So, I had to explicitly specify SSL version in ASIHTTPRequest.m

     const void* keys[] = { kCFStreamSSLLevel };
     // kCFStreamSocketSecurityLevelTLSv1_0SSLv3 configures max TLS 1.0, min SSLv3
     // (same as default behavior on versions before iOS 5).
     // kCFStreamSocketSecurityLevelTLSv1_0 configures to use only TLS 1.0.
     // kCFStreamSocketSecurityLevelTLSv1_1 configures to use only TLS 1.1.
     // kCFStreamSocketSecurityLevelTLSv1_2 configures to use only TLS 1.2.
     const void* values[] = { CFSTR("kCFStreamSocketSecurityLevelTLSv1_0SSLv3") };
     CFDictionaryRef sslSettingsDict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
     CFReadStreamSetProperty((CFReadStreamRef)[self readStream], kCFStreamPropertySSLSettings, sslSettingsDict);
     CFRelease(sslSettingsDict);
 

With these changes, our iPhone app was working as expected.

Acknowledgement

Many thanks for Apple Engineer "Quinn", who helped resolving the SSL issues.


August 17, 2011

Implementing Application Search and Custom Suggestions in Android

Filed under: Android — admin @ 2:44 pm

Being a search company, Google provides search button on most Android devices and allows a simple API to create search bar in the Android application. In addition, you can also use suggestions API to add your own suggestions either from the database or remote service. In this blog, I will demonstrate how to add the search and suggestions to your Android application.

Searchable configuration

The first thing you would need to configure is searchable.xml in res/xml directory. Here is an example file:

 <?xml version="1.0" encoding="utf-8"?>
 <searchable xmlns:android="http://schemas.android.com/apk/res/android"
     android:label="@string/app_name" android:hint="@string/search_hint"
     android:searchMode="showSearchLabelAsBadge"
     android:searchSettingsDescription="suggests symbols"
     android:queryAfterZeroResults="true"
     android:searchSuggestAuthority="com.plexobject.service.SearchSuggestionsProvider"
     android:searchSuggestSelection=" ? " android:searchSuggestIntentAction="android.intent.action.VIEW">
     >
 </searchable>
 

Search Activity

Next you would create your search activity, which would be automatically started when a user hits the hard search button or search option from menu/actionbar.

 package com.plexobject.activity;
 
 import android.app.Activity;
 import android.app.SearchManager;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.util.Log;
 
 import com.plexobject.R;
 
 public class SearchActivity extends Activity {
     private static final String TAG = SearchActivity.class.getSimpleName();
 
     public SearchActivity() {
     }
 
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.search_activity);
         this.setDefaultKeyMode(Activity.DEFAULT_KEYS_SEARCH_LOCAL);
 
         final Intent queryIntent = getIntent();
 
         final String queryAction = queryIntent.getAction();
         if (Intent.ACTION_SEARCH.equals(queryAction)) {
             this.doSearchQuery(queryIntent);
         } else if (Intent.ACTION_VIEW.equals(queryAction)) {
             this.doView(queryIntent);
         } else {
             Log.d(TAG, "Create intent NOT from search");
         }
 
     }
 
     @Override
     public void onNewIntent(final Intent queryIntent) {
         super.onNewIntent(queryIntent);
         final String queryAction = queryIntent.getAction();
         if (Intent.ACTION_SEARCH.equals(queryAction)) {
             this.doSearchQuery(queryIntent);
         } else if (Intent.ACTION_VIEW.equals(queryAction)) {
             this.doView(queryIntent);
         }
     }
 
     private void doSearchQuery(final Intent queryIntent) {
         String queryString = queryIntent.getDataString(); // from suggestions
         if (query == null) {
             query = intent.getStringExtra(SearchManager.QUERY); // from search-bar
         }
 
         // display results here
         bundle.putString("user_query", queryString);
         intent.setData(Uri.fromParts("", "", queryString));
 
         intent.setAction(Intent.ACTION_SEARCH);
         queryIntent.putExtras(bundle);
         startActivity(intent);
     }
 
     private void doView(final Intent queryIntent) {
         Uri uri = queryIntent.getData();
         String action = queryIntent.getAction();
         Intent intent = new Intent(action);
         intent.setData(uri);
         startActivity(intent);
         this.finish();
     }
 }
 
 

Note that you would need to implement viewing logic after the search in doSearchQuery method. Also, if your search service is being called from the search bar, you would get query string from SearchManager.QUERY parameter in the intent and if your search service is being called from the suggestion, the query string would be in getData or getDataString. You would then declare the activity in your AndroidManifest.xml such as:

         <activity android:name=".activity.SearchActivity"
             android:configChanges="orientation|keyboardHidden" android:label="@string/app_name"
             android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.SEARCH" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             <meta-data android:name="android.app.searchable"
                 android:resource="@xml/searchable" />
         </activity>
 

For all the activities that would be using search activity would declare SearchActivity in their definition inside AndroidManifest.xml such as:

         <activity android:name=".activity.MyActivity"
             android:configChanges="orientation|keyboardHidden" android:label="@string/app_name"
             android:launchMode="singleTop">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             <meta-data android:name="android.app.default_searchable"
                 android:value=".activity.SearchActivity" />
         </activity>
 
 

In order to support soft search, I added an option for search to the action-bar as well as menu item and then invoked following method when the search menu was clicked:

     @Override
     public boolean onSearchRequested() {
         startSearch("default-search", true, null, false);
         return true;
     }
 

This would start your search activity described above.

Suggestions Provider

I already added com.plexobject.service.SearchSuggestionsProvider in the searchable.xml file above, which provides custom implementation of suggestions.

 package com.plexobject.service;
 
 import java.util.List;
 
 import android.app.SearchManager;
 import android.content.ContentValues;
 import android.content.SearchRecentSuggestionsProvider;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.net.Uri;
 import android.util.Log;
 
 public class SearchSuggestionsProvider extends SearchRecentSuggestionsProvider {
     static final String TAG = SearchSuggestionsProvider.class.getSimpleName();
     public static final String AUTHORITY = SearchSuggestionsProvider.class
             .getName();
     public static final int MODE = DATABASE_MODE_QUERIES | DATABASE_MODE_2LINES;
     private static final String[] COLUMNS = {
             "_id", // must include this column
             SearchManager.SUGGEST_COLUMN_TEXT_1,
             SearchManager.SUGGEST_COLUMN_TEXT_2,
             SearchManager.SUGGEST_COLUMN_INTENT_DATA,
             SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
             SearchManager.SUGGEST_COLUMN_SHORTCUT_ID };
 
     public SearchSuggestionsProvider() {
         setupSuggestions(AUTHORITY, MODE);
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
 
         String query = selectionArgs[0];
         if (query == null || query.length() == 0) {
             return null;
         }
 
         MatrixCursor cursor = new MatrixCursor(COLUMNS);
 
         try {
             List list = callmyservice(query);
             int n = 0;
             for (MyObj obj : list) {
                 cursor.addRow(createRow(new Integer(n), query, obj.getText1(),
                         obj.getText2()));
                 n++;
             }
         } catch (Exception e) {
             Log.e(TAG, "Failed to lookup " + query, e);
         }
         return cursor;
     }
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         throw new UnsupportedOperationException();
     }
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         throw new UnsupportedOperationException();
     }
 
     @Override
     public int update(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         throw new UnsupportedOperationException();
     }
 
     private Object[] createRow(Integer id, String text1, String text2,
             String name) {
         return new Object[] { id, // _id
                 text1, // text1
                 text2, // text2
                 text1, "android.intent.action.SEARCH", // action
                 SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT };
     }
 
 }
 
 

You may store your suggestions in the database and may need to query the database and return database cursor. However, above provider makes a remote call and then populate Cursor using MatrixCursor from the objects that are returned from the remote service. I omitted actual remote calls that return your suggestions as it can be very application specific. As our implementation does not add anything in the database, it throws exceptions in the insert, update and delete methods. The suggestions provider can support one-line or two-line suggestions and above implementation is using two-line suggestions.
Finally, you will need to add provider and default search activity to your AndroidManifest.xml:

         <provider android:name=".service.SearchSuggestionsProvider"
             android:authorities="com.plexobject.service.SearchSuggestionsProvider"></provider>
         <meta-data android:name="android.app.default_searchable"
             android:value=".activity.SearchsActivity" />
 

As you would be calling remote service you would need following permission in your AndroidManifest.xml: :

         <uses-permission android:name="android.permission.INTERNET" />
 

If you were using database suggesgtion provider, you can add suggestions to the database as:

         SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, SearchSuggestionsProvider.AUTHORITY, SearchSuggestionsProvider.MODE);
         suggestions.saveRecentQuery(queryString, null);
 

and remote suggestions from the database as:

         SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this,
                 SearchSuggestionsProvider.AUTHORITY,
                 SearchSuggestionsProvider.MODE);
         suggestions.clearHistory();
 

Summary

As you can see with a few line of configuration and code you can create application wide search. You can also expose your search to default Android search and you can read Android documentation for more information.


June 21, 2011

Overview of Trading Floor Game for iOS

Filed under: iPhone development — admin @ 4:51 pm



Introduction

The Trading Floor is a social game based on auction-styled stock trading where you trade virtual stocks and options with your friends and family. Each game lasts for a day, where you join a game as a stock broker with objective of buying stocks at low-price and selling at high-price, thus making most money from the trades. Unlike a real-world stock market, each game is limited to a single exchange, which can be public or private. In addition to trading, the Trading Floor brings a lot social aspects to the game such as building private exchanges, which can be only joined by your friends and expressing like-ability for a company, which determines a Buzz for the company. Also, the members can view activities for other players in a game, view their gain/loss from the trading, view ranking of the players in terms of gain/loss and comment on their trades. At the end of a game, the top players for each game are awarded badges, which are permanently visible from their profile.

Getting Started

You can download Trading Floor from Apple AppStore.

iPad iPhone

You would see listing o active games that you can join. A game is started for each exchange in the system which can be public or privately created by the users.

Joining a game

When you click on games, you would see listing of all games such as:


From there you can select the game that you wish to join and you would be prompted to enter a nick name that you can use to join the game such as:

iPad iPhone

If an exchange is private, you would require a password to join and would need to contact the owner of the exchange. After clicking the Join button the Trading Floor would allocate a virtual cash of $10,000 to you and would create a portfolio with stocks and options for ten companies, where five of the companies are chosen from your favorite companies and five companies are randomly chosen. It takes about a minute to populate all the portfolio and then you can start trading. After joining the game you can also browse other players, view activities, portfolio, capital gains, orders and badges. There are also buttons below to quickly select between players, view rankings, floor bids, comments and lookup quotes, e.g.

iPad iPhone

Browsing Players

You can see list of players that have joined the game by touching “Browse Players” option, e.g.

iPad iPhone

When you select the player, you will see more details about the player and view activities, orders, portfolio, capital gains, badges, etc. as displayed above.

Selling Stocks/Options from Portfolio

When you select “Portfolio” option, you would see summary of your available cash and list of all stocks/options you hold such as:

iPad iPhone

When you select “Sell” link, you would be taken to the order screen such as:

iPad iPhone

You can specify your selling price and then select “Sell” button to submit the bid. It would take you to the Floor Bid, which lists all bids for buying and selling stocks/options.

Floor Bids

The Floor Bids show all companies available for buy or sell. When you post bids to sell or buy, the go to the floor bids. You can accept the bids that other players have posted for stock/option sell or buy on the Floor Bids. Note that when you post bids you specify the price for the stock but when you accept bids you don’t negotiate. If you need to change the price for your bids, you can edit your open orders. Also, when you trade stocks or options, you are only trading to the people who are in the same exchange.

iPad iPhone

When another user accepts your bid, the sale is completed and you would collect gain/loss from the trade.

Capital Gains

You can view your overall capital gains as result of trading or portfolio gain because the market price for your stocks went up from the player details, orders or portfolio screen. You can drill deep into your capital gain and find out how much profit/loss you made for each order by selecting Capital Gains option from the home screen or selecting Capital Gains row from the player details, orders or portfolio screen.

iPad iPhone

Quote Lookup

You can lookup stock/option quotes as well as latest news for the companies from Quote-Lookup menu option. You can also buy or sell stocks from the Quote-Lookup screen based on your portfolio.

iPad iPhone

In addition to buying and selling stocks/options, you can express how you feel about a company by liking/disliking/loving/hating. This in turn determines the buzz quote for that company. The more players like a company, the higher the buzz quote value that company would have. Unlike buying and trading stocks/options, which are limited to a single exchange (both public and private), the buzz quote is calculated across all exchanges. However, buzz quote is reset when the Trading Floor market is closed (11pm EST). Here is another benefit of Buzz Quote, the BOT player would buy stocks from you at the Buzz Quote price. So, if a stock becomes popular you can make more profit by selling it to the BOT.

Orders history

You can view your past orders for selling or buying stocks/options by selecting “Orders” option, which would show something like:

iPad iPhone

You can also drill into details for each order by selecting the row for the order such as:

iPad iPhone

Player Rankings

You can view your the rankings of players for each game by selecting “Rankings” option which lists players sorted by the capital gains they earned, e.g.

iPad iPhone

Comments

You can view comments on the game by selecting “Comments” option, e.g.


You can add your own comment by selecting “+” icon and typing your message, e.g.

My Badges

The top players for each game get badges at the end of the game, which are permanently visible under the “Badges” option, e.g.


When you select your badge, you will be taken to the old game that you played and can go back to your portfolio, orders and other details.

Exchanges

The exchanges are places where public companies are listed for trading. You can join exchanges created by your friends or build your exchanges. A unique game is started for each exchange, where member players trade against other member players. You can view both public and private exchanges by selecting “Exchanges” from main menu . You would see three types of exchanges: mine, public and private. Mine would list all exchanges that you have created, public would list all public exchanges and private would list top private exchanges. You can also search private exchanges.

iPad iPhone

You can create your own exchange by selecting “+” icon and typing in the exchange symbol, name and password, e.g.



You can then let your friends know about the exchange symbol and password so that they can join them.

Industries

You can view top level industries by selecting “Industries” from the main menu, e.g.

iPad iPhone

When you select an industry, it would list all companies that belong to that industry.

Companies

You can browse or search over 20,000 companies by selecting “Companies” option from the main menu, e.g.

iPad iPhone

When you select a company, it would show latest quote, news and buzz quote for that company.

Companies Around Me

Trading Floor takes advantage of your geo-location capabilities and finds companies that are located near your location, e.g.

Leader Board

The leader board shows top players with most capital gains for all the games that they have played over last month. This option uses Game Center and requires login to the Game Center. You can post your scores to the leader board by selecting the \”Post Capital Gains\” option from the main menu. Also, you can send match invites to your friends at Game Center.

In the end, I hope you find Trading Floor, a fun game for your friends and family and you may learn a few things about the stock market as well. Enjoy!.

June 1, 2011

Deploying Rails 3.0 App on Amazon EC2

Filed under: EC2 — admin @ 12:56 pm

It’s been a few years since I wrote a short HOW-TO on working with EC2 , but recently I tried to migrate the backend of Trading Floor – Facebook and iOS game I have been developing to EC2. So I am documenting the steps for setting up the EC2 for Rails 3.0.

Pre-requisites

I assume you already signed up for EC2, otherwise go to http://aws.amazon.com/ec2/ to signup. Also, you will need Java 5.0 or above, which you can download it from Oracle.

Download EC2 API Tools

First, download EC2 from http://developer.amazonwebservices.com/connect/entry.jspa?externalID=351 and uncompress it in your root directory.

Create a X.509 Certificate

Next, create a X.509 certificate from the AWS Account section. You can then download your certificate and key safely, e.g. I saved them in .ec2 directory under my home directory. Note that you will not be able to download the key again, so don’t lose it.

Environment Variables

Next, I changed my shell as:

 export EC2_HOME=~/ec2-api-tools-1.4.2.4
 export PATH=$PATH:$EC2_HOME/bin
 export EC2_KEY_DIR=~/.ec2
 export EC2_PRIVATE_KEY=$EC2_KEY_DIR/pk-HFG55OCFPZARA6YHW5JGIE6JFD7EQE72.pem
 export EC2_CERT=$EC2_KEY_DIR/cert-HFG55OCFPZARA6YHW5JGIE6JFD7EQE72.pem
 

Where EC2_PRIVATE_KEY and EC2_CERT points to the X.509 key and certificate I downloaded from the Amazon.

Create a Key-Pair

Then I created a pair of keys as:

 ec2-add-keypair plexobject
 

Create a Security Group

I then created a security group for the server

 ec2-add-group web -d 'Web Server'
 ec2-authorize web -P tcp -p 22 -s 0.0.0.0/0
 ec2-authorize web -P tcp -p 80 -s 0.0.0.0/0
 ec2-authorize web -P tcp -p 443 -s 0.0.0.0/0
 

Finding a basic Ubuntu based AMI

Previously I used S3 based AMI, but Amazon now supports EBS based AMIs that has advantage that any changes to the root survive instances of EC2. I launched EC2 instance with basic Ubuntu 11.0 Natty from http://alestic.com/ as:

 ec2-run-instances ami-06ad526f --instance-count 1 --instance-type m1.small \
 --key plexobject --group web -z us-east-1d -m
 

Where -z describes the availability zone and -m turns on monitoring.

Installing Ubuntu Packages

I then proceeded to install basic packages such as Java, Curl, Git, Build, Ruby (1.8.7) and Rails (3.0.3) based on Rails/Ubuntu docs such as:

 sudo apt-get install openjdk-6-jdk
 sudo apt-get install mercurial
 sudo apt-get install curl git-core build-essential zlib1g-dev libssl-dev libreadline5-dev
 sudo  apt-get install libcurl4-openssl-dev 
 sudo apt-get install ruby
 sudo apt-get install rubygems1.8
 sudo gem install rubygems-update
 sudo update_rubygems
 sudo gem install rails
 

I then edited /etc/profile /etc/bash.bashrc and added environment variables

 export PATH=$PATH:/var/lib/gems/1.8/bin/
 export JAVA_HOME=/usr/lib/jvm/java-1.6.0-openjdk/
 

Next, I installed Sqlite and Mysql:

 sudo apt-get install sqlite3 libsqlite3-dev
 sudo gem install sqlite3-ruby
 sudo apt-get install mysql-server mysql-client
 sudo apt-get install libmysql-ruby libmysqlclient-dev
 

Next, I installed Apache and Passenger:

 sudo apt-get install apache2 apache2-mpm-prefork apache2-prefork-dev
 sudo apt-get install apache2-dev libapr1-dev libaprutil1-dev
 sudo gem install passenger
 sudo /var/lib/gems/1.8/gems/passenger-3.0.7/bin/passenger-install-apache2-module*
 

I then edited /etc/apache2/apache2.conf and added:

 LoadModule passenger_module /var/lib/gems/1.8/gems/passenger-3.0.7/ext/apache2/mod_passenger.so
 PassengerRoot /var/lib/gems/1.8/gems/passenger-3.0.7
 PassengerRuby /usr/bin/ruby1.8
 

and then restarted apache

 /etc/init.d/apache2 restart
 

Creating EBS Volume for Data

Next, I created an EBS volume to store all data such as database tables and Rails application from the AWS Console and then attached it to the instance as:

 ec2-stop-instances i-73ab181d 
 ec2-attach-volume  vol-612eaa0a -i i-73ab181d -d /dev/sdf
 ec2-start-instances i-73ab181d 
 

Note that you have to create the EBS volumen in the same availability zone as your instance. I then logged into my machine using

 ssh -i plexobject.pem ubuntu@ec2-50-19-134-251.compute-1.amazonaws.com
 

and then formatted the newly built EBS volume as:

 sudo fdisk -l
 sudo mkfs -t ext4 /dev/xvdf
 

I then edited /etc/fstab and added

 /dev/xvdf       /data   auto    defaults,nobootwait,noatime     0       0
 

and then rebooted machine

 sudo reboot
 

Moving Mysql Data Directory

Mysql installs data directory on the root volume in /var/lib/mysql directory, which I wanted to move to newly created volume. So I created a directory /data/mysql so I stopped mysql:

 sudo /etc/init.d/mysql stop
 

I then copied mysql data directory such as:

 sudo cp -R -p /var/lib/mysql/mysql /data/mysql
 sudo chown -R mysql:mysql /data/mysql/
 

I didn’t copy entire mysql directory, only mysql subdirectory. Next I edited /etc/mysql/my.cnf and changed datadir to /data/mysql directory and then edited /etc/apparmor.d/usr.sbin.mysqld and changed all /var/lib/mysql to /data/mysql. Finally I restarted AppArmor profiles as:

 sudo /etc/init.d/apparmor reload
 

Then restarted mysql:

 sudo /etc/init.d/mysql restart
 

I changed my root password and created a local mysql user as

 mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
 mysql> GRANT ALL PRIVILEGES ON *.* TO 'tfuser'@'localhost' IDENTIFIED BY 'mypass' WITH GRANT OPTION;
 mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON dbname.* TO 'tfuser'@'localhost' IDENTIFIED BY 'mypass';
 

I copied my app to /data/trading_floor and changed permissions of all files to www-data

 sudo chown -R www-data:www-data /data/trading_floor
 

Then created /etc/apache2/sites-available/tf with

 
         ServerAdmin shahbhat@gmail.com
         ServerName tf.plexobject.com
         DocumentRoot /data/trading_floor/public/
         
                  AllowOverride all
                 Options -MultiViews
                 RailsEnv production
          
 
 

Finally, I restarted apache

 /etc/init.d/apache2 restart
 

Creating Elastic IP address

I wanted a permanent IP address for the server so I created EIP using AWS Console. Then associated my instance with the new IP address:

 ec2-associate-address 50.19.248.7 -i i-73ab181d
 

It rebooted your machine with the new IP address. I then changed DNS zone and pointed tf.plexobject.com to 50.19.248.7 (this may take hours to propagate). Next, I changed my Facebook app’s configuration and iOS app’s configuration to point to tf.plexobject.com.

Creating my EBS Image

Once I was happy with the server configuration, I created EBS image for future use. First, I detached data volume and then created image as follows:

 ec2-stop-instances i-f97ca197
 ec2-detach-volume vol-3f65d954
 ec2-create-image i-f97ca197 -n tf-20110601 -d 'Trading Floor Application Server'
 

I terminated my previous instance as

 ec2-terminate-instances -i  i-f97ca197 
 

and created instance with the new image

 ec2-run-instances ami-80837ae9 --instance-count 1 --instance-type m1.small --key tf --group web -z us-east-1d -m
 

After the launch, you would have to reattach the data volume

 ec2-stop-instances i-73ab181d 
 ec2-attach-volume  vol-612eaa0a -i i-73ab181d -d /dev/sdf
 ec2-start-instances i-73ab181d 
 

Summary

Voila, I had my game application running on the cloud. In order to cut per/hour cost I reserved instances for entire year. I am not quite done with my server and am now working on application specific configuration and adding better monitoring/backup. As, we have learned from recent Amazon Cloud outage that deploying your app on the cloud is only half the work, making it performant, scalable and fault tolerant is other half which is still manual work. Finally, I plan to release the Facebook app for Trading Floor and submit iOS app in a couple of weeks, be sure to try it and send me your suggestions.

April 25, 2011

Review of Guy Kawasaki’s book – “Enchantment: The Art of Changing Hearts, Minds, and Actions”

Filed under: Business — admin @ 12:23 pm

I recently read Guy Kawasaki’s book Enchantment: The Art of Changing Hearts, Minds, and Actions. This book shows how to engage with other people and build better relationships similar to Dale Carnegie’s book How to Win Friends & Influence People. Though, this book covers these topics in more professional context and it includes advice from several other business and management books. As, Guy is also a very savvy social media user, this book covers several tips on using modern networking tools to build personal relationships with others.

Enchantments

Guy describes enchantments as a way of delighting people with a product, service or organization, which is similar to the concept of Customer Delight popular in business literature. Guy suggests to start with a good product or service and fill people with the delight. This also reminded me Tony Hsieh’s book Delivering Happiness: A Path to Profits, Passion, and Purpose.

Likable and Trustworthy

Once you have a good product, you build the enchantments by being likable and trustworthy. The likability chapter covers several pointers such as smile, dress appropriately, firm handshake, accept others, yes attitude, and work in open environment. Guy encourages finding shared interests with other party and creating win-win situation when negotiating. On being trustworthy, Guy suggests giving people benefit of doubt, disclosing interests and positioning yourself. Some of these techniques seemed similar to what I have read from Stephen Covey’s book The 7 Habits of Highly Effective People and from agile development gurus.

Pre-Launch/Premortem

Guy gives a great set of tips on preparation before launching a product and suggests the product should be:

  • great
  • deep
  • intelligent
  • complete
  • empower
  • elegant

Product Launch

On launching a new product, Guy suggests telling personal stories, showing courage, planting many seeds and aspiring people by promising a better world. This chapter reminded me of how Steve Jobs promotes Apple products by promising better future, giving great demo, and simplifying the interface.

Overcoming Resistance

On overcoming resistance, Guy suggests creating perception of ubiquity and scarcity and finding a way to agree, which enhances your chances of being likable. I found the chapter on overcoming resistance a bit weak and encourage readers to look at Switch: How to Change Things When Change Is Hard and Fearless Change: Patterns for Introducing New Ideas.

Enchanting Influencers

Guy offers a great practical guidance on enchanting influencers such as working on grassroots, creating intrinsic motivation, paying it forward, and reciprocity. I liked his advice of saying “I know you’d do same for me” instead of saying “You’re welcome” in response to thank-you”.

Ecosystems

In order to create a grassroots support of your products or services, Guy recommends creating a product worthy of ecosystem and then lists several tools, which encourage exchange of ideas and collaboration such as user-groups, blogs, conferences, reward system, open architecture. Another key factor for ecosystem is having a diversified team, which different roles such as advocate, skeptic, visionary, adult, evangelist and rain maker.

Push Technology

This is one of best chapters in the book and shows how to use modern push technologies such as Presentations, Email, and Twitter. Guy recommends engaging many people fast and often. He also recommends giving them credit and providing a value for them. On presentations, Guy recommends customizing intro based on audience, selling dreams, dramatizing and rehearsing it. He suggests keeping the presentation short with 10-20-30 rule, where presentation has no more than 10 slides, takes 20 minutes and uses no less than 30-size font. For email, Guy suggests keeping it short (under six sentences) and asking for a specific action.

Pull Technology

On pull technologies, Guy suggests creating a website/blog with good content, refreshing contents frequently and having an about page. On Facebook, Guy suggests having a good landing page and being helpful. On Linked-in, Guy suggests having a great profile and reaching out to others actively.

Enchanting Employees

Guy also provides useful set of pointers on being a good employer such as engaging employees by providing MAP (Mastery, Autonomy, Purpose) and empowering employees to do the right things. He recommends instead of judging actions of others against their intentions, be harsh on yourself and judge your results against their intentions. He also suggests celebrating success and includes tips from Good Boss, Bad Boss: How to Be the Best… and Learn from the Worst such as protecting people from intrusions. Guy cites Michael Lopp’s advice from Managing Humans: Biting and Humorous Tales of a Software Engineering Manager such as setting ambitious goals, enabling, appreciating and providing feedback to the employees.

Enchanting Boss

On enchanting boss, Guy recommends:

  • make your boss look good
  • drop everything when boss asks for something
  • under-promise and over-deliver
  • prototype work by completing part of assignment and asking for feedback
  • show and broadcast progress while giving credit to colleagues who helped
  • form friendship
  • ask for mastership
  • deliver bad news early

Resist Enchanters

Finally on resisting enchanters, Guy suggests looking far in future, knowing your limits, having a skeptic attitude and not falling for example of one.

Summary

In this book, Guy Kawasaki provided a good collection of practical advice on building better interpersonal relationships and using tools from social media effectively. It shows that in order to build long lasting relationships, you have to be sincere and always be willing to help others. I found Guy’s pointers on push and pull technologies most helpful as he has created cult of followers on Twitter and Facebook and provided a number of tips from his personal experience.

February 9, 2011

Tips and Idioms for Developing Network based iOS Applications

Filed under: iPhone development — admin @ 12:21 pm

Over the last year, I built a number of iOS applications such as yogaspot, iPlexLotto and OptionsHouse (still in development), which are backed by the network services. I am documenting a few idioms and best practices that I have used in these applications along with some lessons learned:

Separation of concerns

The iOS APIs are based on MVC design pattern, which encourages separating the non-GUI code such as Models, Services, Helper classes from your views and uses Controllers to communicate between GUI and non-GUI code. As with building any other kind of application, keeping your design and code clean would make your application more maintainable and extendable. The iOS APIs make heavy use of protocols for interface segaration principle. Here are some of the usages of protocols in iOS APIs:

Data Sources

The iOS API use protocols for data sources, which then populate views such as table views, pickers and other views. In most application, your controller will be the data source but I used the data source protocols to create Wizard like behavior by implementing the data source interface in multiple classes and displaying different contents based on the state of application.

Delegates

The delegates are callback interface that are generally used for communication between classes or threads. As, protocols in Objective C can define both mandatory and optional behavior, delegates are also used to provide customized UI behavior. The iOS SDK uses delegates for customizing UI in a number of classes such as UISearchDisplayDelegate, UISearchBarDelegate, UIGestureRecognizerDelegate, UITableViewDelegate, etc. Here is an example of delegate for communication between classes:

 @protocol OrderCallbackDelegate
 @required
 - (void) addedOrder: (Order *)order;
 @end
 
Implementing Delegate

@protocol OrderCallbackDelegate
@interface OrderCallbackDelegate : NSObject {
}
@end

Referencing Delegate
 @interface SomeObject : NSObject {
     id delegate;
 }
 @property(nonatomic, retain) id delegate;
 @end
 

Encapsulation and Extensions

The iOS provide several options for hiding private details or extending existing classes. For example, you can use @private keyword in the class declaration to enforce encapsulation such as:

 
 @interface MyClass : NSObject {
     @private
     int myVar;
 }
 @end
 

The myVar attribute can only be accessed by the instance of MyClass.

Objective C supports inheritance and composition as other object-oriented languages, which I won’t cover here. However, the Categories feature of Objective C language is a more powerful way to extend an existing class without modifying it. For example, you can extend UIColor to provide additional colors as follows:

 @interface UIColor(MyBlueColor)
 +(UIColor *)myBlueColor;
 @end
 
 @implementation UIColor(MyBlueColor)
 +(UIColor *)myBlueColor {
     return [UIColor colorWithRed:0.2274 green:0.4666 blue:0.8235 alpha:1.000];
 }
 
 @end
 
 

Other examples include JSON parsing library, which extends NSString to support JSON parsing.
In addition to extension, you can also use categories for encapsulation by using categories syntax in the implementation file and adding private method and properites, e.g.

 @interface MyClass(private)                                                                                                                                                                                                                                                 
 //my private methods only visible in the implementation file
 @end
 

Code Structure

As with any other application, keeping your code organized in your iOS project helps readability and maintenance. One thing I don’t like about XCode is that when you create groups for organizing classes in your XCode project, it doesn’t create folders, so you will find all classes in Classes directory. So, I prefer creating folders on the file system and then add folders as groups to XCode. Here is an example of different groups I used in my projects:

  • Models
  • Network
  • Controllers
  • Utils
  • OtherSources
  • Parsing
  • Views
  • Resources

Dynamic Language Support

Objective C supports both static and dynamic language syntax and has powerful support of reflection. This gives you powerful tool to write concise code. For example, I could not find any XML or JSON binding library for Objective C, so I built helper classes that parsed XML and JSON and automatically bound the properties to the classes that used same names, e.g.

 typedef id (^objectFactoryBlk_t)();
 
 @interface JsonParser : NSObject {
 }
 + (void)bindJson:(NSData *)data toObject:(NSObject *) object;
 + (void)bindJson:(NSData *)data toArray:(NSMutableArray *) array mandatoryFields:(NSArray *)fields withFactory:(objectFactoryBlk_t) objectFactoryBlk;
 
 @end
 

And

 
 @interface XmlParser : NSObject {
     xmlDocPtr doc;
 }
 - (id)initWithXML:(NSData *) document;
 + (void)bindXml:(NSData *)xml toObject:(NSObject *) object;
 + (void)bindXml:(NSData *)xml toArray:(NSMutableArray *) array  mandatoryFields:(NSArray *)fields withFactory:(objectFactoryBlk_t) objectFactoryBlk;
 @end
 

I am skipping more details but you can download the implementation from Parser.zip.
Also, as Objective-C is dynamic languages, the compiler won’t complain if you call methods that are not defined, so you need to pay special attention to warnings when using dynamic language constructs or better turn warnings into errors by configuring your build. You can also use CLANG static analyzer to find improper dynamic method.

Network Services

Any network based iPhone app would rely on the backend network services and you would make these easier if these services are REST based. I recommend ASIHTTPRequest library, which supports asynchronous communication and concurrency.

Loading Status

When designing an modal dialog that interacts with the backend, it helps if you show loading message or use UIActivityIndicator when making network request (even though the network request would be asynchronous). For example, the OptionsHouse app shows loading status when executing trades as it may take a couple of seconds to communicate with the server.

XML vs JSON vs Binary

When you are developing an iPhone application, you will learn that despite the increasing powerful hardware it’s not same as desktop, laptops or servers we have in the office. Thus, you will have to learn how to optimize every aspect of the application and one of the key factor for network application is the protocol for the payload. I used both XML and JSON based services and found that JSON parsing was at least twice as fast compare to XML. Optionally, you can use Binary format such as binary plist, which further improves the performance of parsing.

Network Latency

An iPhone user might be connected on Edge, 3G, 4G or WiFi network and the network speed would vary. Though, you can make a general assumption that WiFi would be faster than 3G/4G, which would be faster than Edge, but I suggest that you test the speed within the app instead of relying on the connection. For example, you might be using WiFi at a local coffee shop or a conference, where the speed is slower than the edge or 3G.

Network Outages

On websites, the network services are often behind the web server so when they are down it’s easy to post a page for network outage. You will have to handle such outages in the application and notify the users about the outages.

Network Data Requests

A network based application requires fetching the data from the server and it’s best that you communicate with the services without blocking user interface. Both ASIHTTPRequest and NSURL supports asynchronous communcationions, where you register callback methods when the data arrives or network failure occurs. I built a thin layer on top of ASIHTTPRequest so that I could use blocks:

 typedef void (^requestCompletedBlk_t)(id payload);
 typedef void (^requestFailedBlk_t)(NSError *error);
 
 @interface ServiceBlocks : NSObject {
     requestCompletedBlk_t requestCompletedBlk;
     requestFailedBlk_t requestFailedBlk;
     ASIHTTPRequest *request;
     NSTimeInterval started;
     int retries;
 } 
 @property (copy, nonatomic) requestCompletedBlk_t requestCompletedBlk;
 @property (copy, nonatomic) requestFailedBlk_t requestFailedBlk;
 @property (retain, nonatomic) ASIHTTPRequest *request;
 @property (assign, nonatomic) NSTimeInterval started;
 @property (assign, nonatomic) int retries;
 
 @end
 
 @implementation ServiceBlocks
 @synthesize requestCompletedBlk, requestFailedBlk, request, started, retries;
 -(id)initWithRequest:(ASIHTTPRequest *) arequest completedBlock:(requestCompletedBlk_t) arequestCompletedBlk failedBlock:(requestFailedBlk_t) arequestFailedBlk {
     if(self = [super init]) {
         self.requestCompletedBlk = arequestCompletedBlk;
         self.requestFailedBlk = arequestFailedBlk;
         self.request = arequest;
         self.started = [[NSDate date] timeIntervalSince1970];
         self.retries = 0;
     }
     return self;
 }
 
 - (void)dealloc {
     [requestCompletedBlk release];
     [requestFailedBlk release];
     [request release];
                                                                                                                                                                      
     [super dealloc];
 }
 @end
 

Here is how you can use ASIHTTPRequest to make REST POST and GET requests:

 - (void)postJson:(NSData *) json completedBlock:(requestCompletedBlk_t)requestCompletedBlk failedBlock:(requestFailedBlk_t) requestFailedBlk {                       
     NSURL *url = [NSURL URLWithString:[[Configuration sharedConfiguration] getJsonServerUrl]];
     NSString *jsonBuffer = [[[NSString alloc] initWithData:json encoding:NSASCIIStringEncoding] autorelease];
     
     ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
     request.connectionCanBeReused = YES;
     [request setUseKeychainPersistence:YES];
     [request setTimeOutSeconds:60];
     [request setValidatesSecureCertificate:NO];
     [request setDelegate:self];
     [request setShouldPresentAuthenticationDialog:YES];
     [request setDidFinishSelector:@selector(downloadCompleted:)];
     [request setDidFailSelector:@selector(downloadFailed:)];
     [request appendPostData:json];
 
     [request addRequestHeader:@"Content-Type" value:@"application/json"];
     [request setRequestMethod:@"POST"];
     [request buildPostBody];
     ServiceBlocks *blocks = [[ServiceBlocks alloc] initWithRequest:request completedBlock:requestCompletedBlk failedBlock:requestFailedBlk];
     [requests setValue:blocks forKey:[self toKey:request]];
     [blocks release];
     [request startAsynchronous];
 }
 
 
 - (void)get:(NSString *) uri completedBlock:(requestCompletedBlk_t) requestCompletedBlk failedBlock:(requestFailedBlk_t) requestFailedBlk {
     NSURL *url = [NSURL URLWithString:uri];                                                                                                                          
     ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
     [request setUseKeychainPersistence:YES];
     [request setDelegate:self];
     [request setShouldPresentAuthenticationDialog:YES];
     [request setDidFinishSelector:@selector(downloadCompleted:)];
     [request setDidFailSelector:@selector(downloadFailed:)];
 
     ServiceBlocks *blocks = [[ServiceBlocks alloc] initWithRequest:request completedBlock:requestCompletedBlk failedBlock:requestFailedBlk];
     [requests setValue:blocks forKey:[self toKey:request]];
     [blocks release];
     [request startAsynchronous];
 }
 
 
 

Above methods setup downloadCompleted as a callback handler for successful completion of the request and downloadFailed for service failure, which are defined as follows:

 - (void)downloadFailed:(ASIHTTPRequest *)theRequest {
     ServiceBlocks *blk = [requests valueForKey:[self toKey:theRequest]];
     if (blk != nil) {
         NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - blk.started;
         NSLog(@"#####downloadFailed for %@ in %f seconds", [self toKey:theRequest], interval);
         
         if (blk.requestFailedBlk != nil) {
             blk.requestFailedBlk([theRequest error]);
         }
         [requests removeObjectForKey:[self toKey:theRequest]];
     } else {
         NSLog(@"#####downloadFailed could not find blocks for %@, available %@", [self toKey:theRequest], [requests allKeys]);
     }
     
     [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
 }                                                                                                                                                                    
 
 - (void)downloadCompleted:(ASIHTTPRequest *)theRequest {
     ServiceBlocks *blk = [requests valueForKey:[self toKey:theRequest]];
     if (blk != nil) {
         NSTimeInterval interval = [[NSDate date] timeIntervalSince1970] - blk.started;
         NSLog(@"################################downloadCompleted for %@  in %f seconds", [self toKey:theRequest], interval);
 
         NSData *content = [theRequest responseData];
         if (blk.requestCompletedBlk != nil) {
             blk.requestCompletedBlk(content);
         }
         [requests removeObjectForKey:[self toKey:theRequest]];
     } else {
         NSLog(@"################################downloadCompleted could not find blocks for %@, available %@", [self toKey:theRequest], [requests allKeys]);
     }
 
     [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
 
 }
 

Both methods lookup the user’s block that was passed upon request and then callback the proper block. In certain cases, I extended downloadFailed method to automatically retry the request upon certain failure types such as timeout errors.

Gateway and Proxy Patterns

I used Gateway and Proxy patterns to wrap remote services in a local class that defined methods using local Model classes and automatically converted Models into proper XML/JSON format for requests and responses. This kept the code clean and in a few cases, I had to switch from XML to JSON, which was made in these classes and rest of application remained unchanged.

Check-If-Modified

As both iPlexLotto and OptionsHouse refreshed data continuously in background, I needed an efficient way to check if the data is updated on the server. HTTP provides a great way to reduce bandwidth by using E-Tag or Last-Modified headers, and we used similar techniques to send modified timestamp as part of the response and used it in future requests. The server then compared the timestamp in the request with local state and sent response only if the data was modified.

Network Errors

A critical aspect of network based applications is handling network errors. There can be various reasons for the network errors such as phone might be in Airplane mode so no network requests should be made. In other cases network connection might be too slow or the service is down. Also, you may be continuously updating data in background and in some cases occasional network interruptions can be ignored. This requires careful consideration of the UI so that the application is smart about the environment and retries when possible instead of annoying users with popup alerts.

Caching

A quick way to improve performance is to cache contents for a certain period of time depending on the data. As iOS devices have limited memory, I used in-memory cache for small objects and used file based cache for larger objects. For example, I cached lottery results for a couple of hours, on the other hand for trading application I cached orders and positions for a few seconds. I didn’t’ cache quotes data as it changed frequently and showing stale quotes can result in financial loss. In addition to the user data, I also cached chart images or other meta-data that is not frequently changed. Here is a simple implementation of file based cache:

 
 #define K_CACHE_TIMEOUT_SECS 3600
 @interface FileCache : NSObject {
 }
 
 +(BOOL)saveObject:(NSString *)name withData:(id)entry;
 +(id)loadObject:(NSString *)name expired:(BOOL*)expired;                                                                                                                                                                                                                      
 
 @end
 
 
 #import "FileCache.h"
 
 @implementation FileCache
 
 +(id)loadObject:(NSString *)name expired:(BOOL*)expired {
     *expired = YES;
     NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
     NSString *filePath = [docs stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.obj", name]];
     NSError *error;
     NSFileManager *fileManager = [NSFileManager defaultManager];
     NSDictionary *attrs = [fileManager attributesOfItemAtPath:filePath error:&error];
     id obj = nil;
     if (attrs != nil && [attrs count] > 0) {
         NSDate *date = [attrs valueForKey:@"NSFileModificationDate"];
         if (date != nil) {
             NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:date];
             if (interval <= K_CACHE_TIMEOUT_SECS) {
                 *expired = NO;
             }
             obj = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
         }
     }
     return obj;
 }
 
 
 +(BOOL)saveObject:(NSString *)name withData:(id)entry {
     NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
     NSString *filePath = [docs stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.obj", name]];
     return  [NSKeyedArchiver archiveRootObject:entry
                                               toFile:filePath];
 }
 @end
 

Using Timers for Repeatable Tasks

Both iPlexLotto and OptionsHouse continuously updated the data in background. For example, the OptionsHouse application fetched stock/option quotes, order transactions, positions in background. I used GCD's based Timers to schedule periodic download of the data:

 dispatch_time_t now = dispatch_walltime(DISPATCH_TIME_NOW, 0);
 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));                                                                                                                                                               
 int kPOLL_INTERVAL_SECS = 5;
 dispatch_source_set_timer(timer, now, [kPOLL_INTERVAL_SECS * NSEC_PER_SEC, 5000ull);
 dispatch_source_set_event_handler(timer, ^{
     // fetch data and update
     });
 

Above timer runs every 5 seconds and polls for new data and download updates for the user. In my applications, I activated the timer when the application comes in foreground (applicationDidBecomeActive method) and disable it when it goes in background (applicationWillResignActive method). Note that this syntax is much simpler compared to prior syntax of NSTimer and NSRunLoop.

Multi-tasking

Apple added support for multi-tasking in iOS SDK 4.0, which means an application can now be in one of these states

Active

When an application is in foreground and interacting with the user.

InActive

When an application is on its way to background and stops receiving the events.

Background

When an application is running in background.

Suspended

When application is still in memory but not executing.

Callback Methods

The iOS SDK added a number of callback methods for the state transfer such as:

 - (void)applicationWillResignActive:(UIApplication *)application {
     /*
      Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone
  call or SMS message) or when the user quits the application and it begins the transition to the background state.
      Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
      */
 }
 
 - (void)applicationDidEnterBackground:(UIApplication *)application {
     /*
      Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to it
 s current state in case it is terminated later. 
      If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
      */
 }
 
 - (void)applicationWillEnterForeground:(UIApplication *)application {
 }
 - (void)applicationDidBecomeActive:(UIApplication *)application {
 }
 

On the older devices without multi-tasking support, applicationWillTerminate is called after applicationDidEnterBackground.

Multi-Threading

The iOS SDK provides multiple ways to add concurrency support to your application.

NSThread

You can create NSThread as follows

 NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                         selector:@selector(myThreadMainMethod:)
                                         object:nil];
 [myThread start];  // Actually create the thread
 

However, there is an easier way to create a thread by invoking a custom method on an object as follows:

 [NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
 

Alternatively, you can start a background task as follows:

 [myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
 

You will then define your method for doing the work as follows:

 
 - (void)myThreadMainRoutine
 {
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
  
     // Do thread work here.
  
     [pool release];  // Release the objects in the pool.
 }
 

Note that you will have to create an auto-release pool before doing the actual work. Though, these APIs are simple to use but managing threads manually is harder and you have to synchronize shared data, cleanup upon termination or when the app goes in background. See Apple's documentation for more details.

NSOperationQueue

NSOperationQueue is much easier way to manage concurrency as you don't have to create threads manually. For example, you can create an operation to save data asynchronously as follows:

 NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(saveData:) object:data];
 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
 [queue addOperation:operation];
 

Instead of using NSInvocationOperation, you can define an operation class as follows:

 @interface SaveOperation : NSOperation {
     NSData *data;
 }
  
 @property(retain) NSData *data;
  
 - (id)initWithData:(NSData*)data;
  
 @end
 
 @implementation SaveOperation
  
 @synthesize data;
  
 - (id)initWithData:(NSData*)theData;
 {
     if ([super init]) {
         self.data = theData;
     }
     return self;
 }
  
 - (void)dealloc {
     [data release], data = nil;
     [super dealloc];
 }
  
 - (void)main {
     // saving
 }
  
 @end
 

You will then add the operation as follows:

 SaveOperation *op = [[SaveOperation alloc] initWithDataL:data];
 [queue addOperation:op];
 [op release];
 

You can also control the number of concurrent operations via:

 [queue setMaxConcurrentOperationCount:2];
 

Grand Central Dispatch

By far, the easiest way to add concurrency is to use Grand Central Dispatch (GCD) syntax and libraries. The GCD allows you to define tasks via function or blocks and schedules them to the available processors and automatically uses threads behind the scene. Though, it's similar to NSOperation, but the syntax is very terse and the performance is much better than using threads directly.

 dispatch_async(dispatch_get_global_queue(0, 0), ^{
     // save data
     NSLog(@"finished saving data");
  });
 

You can also nested blocks such as

 dispatch_async(dispatch_get_global_queue(0, 0), ^{
     // save data
     NSLog(@"finished saving data");
     dispatch_async(dispatch_get_main_queue(), ^{
         [self.tableView reloadData]
     });
     
  });
 

In addition to using predefined queues, you can also create your own queues, which execute the tasks in serial order such as:

 dispatch_queue_t myQueue = dispatch_queue_create("com.plexobject.myqueue", NULL);
 dispatch_async(myQueue, ^{
     // save data
     //  ...
     // now release the queue (optionally)
     dispatch_release(myQueue);
  });
 

Note that if we need to delete the queue, we must do it after all tasks are finished executed. See Apple's documenation for more details on queues.

Final word on Multi-threading

When using multi-threading explicitly or implicitly, you have to synchronize shared data. The IOS SDK provides several constructs such as @synchronized keyword, mutexes, locks, etc. to protect shared data. Nevertheless, this can be error-prone and can slowdown your application, so eliminate shared data if possible. Also, pay special attentions to exceptions within the thread method and cleanup threads upon app termination or when it becomes inactive. See Apple's documentation for more details. Also, any operations that require UI interaction must be run via main thread, though there are some exceptions such as some QuickTime APIs can be run in background.

Event Notifications

The iOS supports notifications for intercommunication between classes and threads, which promote loosely coupled design. In my applications, I extended notification by creating my own events such as:

 @interface MyEvent : NSObject {
     NSString *type;
 }
 
 @property(nonatomic, copy) NSString *type;
 -(void)fireEvent;
 @end
 
 #import                                                                                                                                                                                                                                                  
 
 #import "MyEvent.h"
 
 
 @implementation MyEvent
 
 @synthesize type;
 
 -(void)fireEvent {
     dispatch_async(dispatch_get_main_queue(), ^{
         [[NSNotificationCenter defaultCenter] postNotificationName:type object:self];
     });
 }
 

@end

Note that the event is being fired on the main thread so that UI controllers can respond to the event and update UI without any changes. The controller class would register for the event (typically in viewDidLoad) as follows:

 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMyevent:) name:@"MyEvent" object:nil];
 

Similarly, controller would unregister for the event in viewDidUnload as follows:

 [[NSNotificationCenter defaultCenter] removeObserver:self name:@"MyEvent" object:nil];
 

The controller would then define the method handler for the event as follows:

 - (void) handleMyevent:(NSNotification *)notif {
     MyEvent *event = (MyEvent *) [notif object];
 }
 

You would then generate the event as follows:

 MyEvent *event = new MyEvent();
 event.type = @"MyEvent"
 event.fireEvent;
 

sending bug reports Automatically

Though, you can download crash reports from the Apple website, I added automatic reporting mechanism for bug reports and crashes. I used UncaughtExceptionHandler class to handle unexpected exceptions and automatically send bug reports. I don’t try to recover from unexpected exceptions as often the memory corruption leave the application in such unstable condition that it’s better to restart the application.

 
 - (void)validateAndSaveCriticalApplicationData:(NSException *)exception {
     NSMutableString *message = [[[NSMutableString alloc] initWithCapacity:200] autorelease];
     [message appendFormat:@"{name:'%@', ", exception.name];
     [message appendFormat:@"reason:'%@', ", exception.reason];
     [message appendFormat:@"userInfo:'%@', callStack:[", exception.userInfo];
     int count=0;
     for (NSObject *obj in exception.callStackReturnAddresses) {
         if (count > 0) {
             [message appendString:@","];
         }   
         [message appendFormat:@"callStack_%d:'%@'", obj];            
     }   
     [message appendString:@"], callSymbols:["];
     count=0;
     for (NSObject *obj in exception.callStackSymbols) {
         if (count > 0) {
             [message appendString:@","];
         }   
         [message appendFormat:@"callSymbol_%d:'%@'", obj];            
     }   
     [message appendString:@"]}"];
 
     [self sendBug:@"Crash report" withMessage:message];
 }
 

Persistence

The iOS SDK provides several options for persistence such as user preferences, file I/O, serialization, sqlite and CoreData. I used file based serialization for caching resources and objects that implemented NSCoding protocol and used CoreData for more complexed data. I will skip file based serialization as it’s fairly straightforward, but would list some tips on CoreData.

  • Setup versions for your data mapping files so that you can easily upgrade schema. You can add the version by selecting xcdatamodel file, choosing Design and then selecting “Add Model Version” from “Data Model” submenu. In order to auto-upgrade migration, define persistentStoreCoordinator accessor as
     
     - (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
         if (persistentStoreCoordinator != nil) {
             return persistentStoreCoordinator;
         }   
         
         NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]; 
         NSURL *storeUrl = [NSURL fileURLWithPath: [docs stringByAppendingPathComponent: @"MyData"]];
         
         NSError *error = nil;    
         NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:                                                                                                                                                                                                       
                                  [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,    
                                  [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
         
     
         persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
         if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
             NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
             abort();
         }    
         return persistentStoreCoordinator;
     }
     
  • Use a single NSManagedObjectContext per thread or queue. For example, I used different NSManagedObjectContext for background timer that updated the database than the NSManagedObjectContext that displayed the table using NSFetchedResultsController.
  • Merge changes when using multiple NSManagedObjectContext objects, e.g.
     
     NSNotificationCenter *dnc = [NSNotificationCenter defaultCenter];
     
     
     [dnc addObserverForName:NSManagedObjectContextDidSaveNotification object:insertManagedObjectContext
                               queue:nil usingBlock:^(NSNotification *saveNotification) {
                                   [[self readManagedObjectContext] mergeChangesFromContextDidSaveNotification:saveNotification];
         }];
     
     // update here
     
     [dnc removeObserver:self name:NSManagedObjectContextDidSaveNotification object:ctx];
     
     
  • In found problems with caching NSManagedObjects when using multiple NSManagedObjectContexts as iOS invalidate existing references when new updates come and you get wonderful error:
    The NSManagedObject with ID:#### has been invalidated.

I ended up creating transient copy of the object for caching purpose. Another factor you will have to consider is what information should be kept in memory and what information can be stored in the database. In general, if the data is small or frequently updated such as stock quote, I kept it in memory, however if the data is large and less often updated such as stock positions, I kept it in the database. In addition, I used the database as interface between UI and network, so the data would get updated asynchronously in background and stored in the database. The network code would then fire an event, which the UI subscribes and would then update the view from the database. This provided fast and efficient way to view large amount of data without waiting.

Performance

Performance is a key part of usage experience. Apple provides a number of tools such as Shark and Instruments to measure performance bottlenecks as well as investigate memory leaks and CPU activity, file I/O, database and other issues. You need to start measuring the performance on real devices early in the application lifecycle so that you can design the application properly.

Memory Management

As, Apple does not support garbage collection on iOS devices, the memory management is one of most painful aspects of the development. I felt that pain as I have been using language such as Java, Ruby, Erlang and Python for past many years that come with garbage collection. Here are some of the tips for memory management:

  • Use properties and synthesize keywords to automatically generate proper accessors/setters.
  • When assigning properties within the class use the syntax “self.property = aproperty” instead of “property = aproperty” so that you invoke your setters properly.
  • Use retain and release instead of dealloc method.
  • Use retain for all object references.
  • Use assign for primitive references.
  • Use copy for String types, GCD blocks and other instances where you want clone of the objects.
  • Use assign when you are using delegate pattern to hold another controller for callback so that you don’t end up with cyclic references.
  • Use AutoRelease Pools for separate threads.
     
     NSAutoreleasePool *pool = [ [ NSAutoreleasePool alloc ] init ];
      // ...
     [pool drain];
     
  • Use auto-release objects sparsely, for example if you only need an object in a method or a block and then just allocate it at the beginning and release it at the end (possibly in finally block of try-catch).
  • If a method name begins with alloc, copy, or new, then you must return allocated instance and caller would own the reference, otherwise you must return an auto-released object.

Use Memory-Leak detection feature of Instruments to find the objects that are not properly released. You can set NSZombieEnabled flag to true on your executable, which can help track the memory leaks when the app crashes. Finally, multi-threading further complicates the memory management. For example, you may pass auto-released object to another thread and by the time that the thread uses it, the first thread’s auto-release pool released it. So, you must retain the object in separate thread before processing it.

Testing

Though, iPhone Simulator is fast and easy way to test the application during the development, but there are significant difference between the iPhone Simulator and real devices. First, many of the APIs such as Location, Apple Store, Accelerator, Email are not supported on the Simulator, but most importantly the Simulator runs on blazing fast MacPro machine, whereas real devices are slow and limited. I highly recommend start your testing with real devices early in your app lifecycle. There is also a big difference in hardware between different generation of iPod touch, iPhone and iPad. For example, iPhone 3G has 620Mhz processor with 128M memory, iPhone 3GS has 833Mhz with 256M memory and latest iPhone 4 has 1Ghz with 512M memory. I suggest using older devices to really find performance and stability issues with the application. Also, you want it to run on most devices so you should target lowest device that you can, which you can configure it by setting target OS property of the build.

Though, Apple provides basic Unit Test support, but I found it very painful to use. We have used the UI Automation tool for functional testing with some success, but it lacks good documentation.

Conclusion

In this blog, I listed several techniques for designing and developing iOS applications, though most of the techniques can be easily applied to other mobile platforms. Though, hardware on mobile devices is becoming more powerful, but it is still constrained with limited memory. You have to pay an exceptional attention to the memory usage, performance and stability of the app and users are going to hate if it’s slow or crashes. The network apps have to further address the availability and speed of the network so that apps is not completely unusable in airport mode and with slow connection. In the end, testing on real devices and real environment is the key to create effective network based applications.



September 22, 2010

An implementation of Virtual Node Router with Consistent Hash algorithm

Filed under: Java — admin @ 1:02 pm

Since the Dynamo paper, published a few years ago, DHTs and consistent hash have become mainstream. Here is my implementation of a virtual node router that uses consistent hash algorithm for splitting requests to the virtual nodes:

 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
 public class VirtualNodeRouter {
     interface HashCalculator {
         long calculateHash(String key);
     }
 
     private static class VirtualNode {
         final String nodeName;
         final int replicaNumber;
 
         VirtualNode(final String nodeName, final int replicaNumber) {
             this.nodeName = nodeName.toLowerCase();
             this.replicaNumber = replicaNumber;
         }
 
         boolean matches(String host) {
             return nodeName.equalsIgnoreCase(host);
         }
 
         @Override
         public String toString() {
             return nodeName + ":" + replicaNumber;
         }
     }
 
     private final HashCalculator hashFunction;
     private final SortedMap<Long, VirtualNode> virtualNodePoolByHash = new TreeMap<Long, VirtualNode>(
             new Comparator<Long>() {
                 public int compare(Long i, Long j) {
                     if (i > j) {
                         return 1;
                     } else if (i < j) {
                         return -1;
                     } else {
                         return 0;
                     }
                 }
             });
 
     public VirtualNodeRouter() {
         this(new HashCalculator() {
             public long calculateHash(String key) {
                 try {
                     MessageDigest sha1 = MessageDigest.getInstance("SHA1");
                     sha1.update(key.getBytes());
                     byte[] digest = sha1.digest();
                     return bytesToLong(digest);
                 } catch (Exception e) {
                     throw new RuntimeException(e);
                 }
             }
         });
     }
 
     public VirtualNodeRouter(final HashCalculator f) {
         this.hashFunction = f;
     }
 
     /**
      * Adds a node with one replica
      * 
      * @param node
      *            - node name
      */
     public void add(String node) {
         add(node, 1);
     }
 
     /**
      * Adds a node to the available pool
      * 
      * @param node
      *            - node name
      * @param replicas
      *            - # of replicas - increase # of replicas based on the
      *            computing power of the machine
      */
     public void add(String node, int replicas) {
         // Note: You can call this method incrementally by adding more replicas,
         // so that you don't cause DOS on
         // your own services
         int existingReplicas = getReplicas(node);
 
         for (int i = 0; i < replicas; i++) {
             VirtualNode virtualNode = new VirtualNode(node, i
                     + existingReplicas);
             virtualNodePoolByHash.put(hashFunction.calculateHash(virtualNode
                     .toString()), virtualNode);
         }
     }
 
     /**
      * remove the node from available pool
      * 
      * @param node
      */
     public void remove(String node) {
         Iterator<Long> it = virtualNodePoolByHash.keySet().iterator();
         while (it.hasNext()) {
             Long key = it.next();
             VirtualNode virtualNode = virtualNodePoolByHash.get(key);
             if (virtualNode.matches(node)) {
                 it.remove();
             }
         }
     }
 
     public String getNode(String key) {
         if (virtualNodePoolByHash.isEmpty()) {
             return null;
         }
         long hash = hashFunction.calculateHash(key);
         for (Map.Entry<Long, VirtualNode> e : virtualNodePoolByHash.entrySet()) {
             if (hash < e.getKey()) {
                 return e.getValue().nodeName;
             }
         }
         SortedMap<Long, VirtualNode> tailMap = virtualNodePoolByHash
                 .tailMap(hash);
         hash = tailMap.isEmpty() ? virtualNodePoolByHash.firstKey() : tailMap
                 .firstKey();
         return virtualNodePoolByHash.get(hash).nodeName;
     }
 
     public void dump() {
         for (Map.Entry<Long, VirtualNode> e : virtualNodePoolByHash.entrySet()) {
             System.out.println("  " + e.getKey() + " => " + e.getValue());
         }
     }
 
     public int getReplicas(String nodeName) {
         int replicas = 0;
         for (VirtualNode node : virtualNodePoolByHash.values()) {
             if (node.matches(nodeName)) {
                 replicas++;
             }
         }
         return replicas;
     }
 
     private static long bytesToLong(byte[] b) {
         ByteBuffer bb = ByteBuffer.wrap(b);
         return bb.getLong();
     }
 }
 

The virtual nodes are added by specifying the node name and the number of replicas. You can add more virtual nodes for the more powerful machines than the low-end machines. You can call remove method when the node goes down or is unavailable. The getNode is called when a request needs to be routed. For example, if you are caching results of a service, you can use the key of cache to find the virtual node and then store the cache value at that node. You can read following resources to learn more about the consistent hash algorithm:



September 16, 2010

My impression of Diaspora codebase

Filed under: Computing — admin @ 5:07 pm

I briefly reviewed the Diaspora source code, which is all written in Ruby on Rails 3.0. Here are my initial thoughts on the codebase:

What I liked:

  • It’s all open source — YES!
  • Diaspora uses latest Rails 3.0 APIs such as global respond_to in controllers, thin controllers, and fat models with new query APIs. The code uses RSpec for tests and Factory-Girl for creating test objects instead of fixtures, which are much easily managed. There are a few Selenium tests, but most of the tests are in RSpec.
  • Diaspora is built using latest technologies and standards such as PKI (OpenSSL), HTML5, VCard, Websockets, Microformat, XRDS, PubSubHubbub along with popular libraries such as EventMachine, HAML, jQuery, Fancybox, Sprinkle, Bundler, Blueprint, etc.
  • Deployment scripts and documentation – Though, there isn’t any documentation on overall architecture or comments in the code, but I found installation documentation very helpful. Also, the deployment rake tasks and configurations are all included in the source code.

What I disliked:

Though, I found the code to be fairly easy to read and consistent in style I found following problems related to the performance, scalability and modularity.

  • Service API – Diaspora uses Rails controller for serving HTML as well as JSON, XML and XRDS requests but I would have preferred separate services for the API with much more defined contract.
  • Pagination – I found sporadic use of pagination in the code but a number of classes use Rails’ builtin relationships without any pagination. It’s like when you ask for banana you get the gorilla holding the banana. In my experience, this has been problem with all O/R mapping tools, which give you nice syntax for fetching related objects until your server runs out of memory or your database dies on you.
  • Before/after filters – I found a number of such filters in the code, which I have found to be another common issue with the scalability when before/after filters require a lot of overhead.
  • Asynchronous Messaging – I didn’t see any use of asynchronous messaging as a lot of requests such as adding/removing a friend can be done asynchronous.
  • Modularity – Diaspora code uses modules and models for the business logic, but I found a couple of models such as user to be too big, which can be divided into suitable modules.
  • MongoDB – I have nothing against MongoDB but I found a lot of code depends on MongoDB. I would have preferred using data access service instead, which completely encapsulates underlying storage technology and allows you to replace it without modifying all the code.

Conclusion

Despite the Knuth’s advice: “Forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil,” you need the architecture for scalability and performance when you are building the platform to replace Facebook. As social networking tools depend much more on Network effects or Metcalfe’s law instead of best technology, I hope early release of the software allows it to capture more users who use it. I was somewhat disappointed that identi.ca has not caught much attraction as open source alternative to Twitter. And I hope that Diaspora succeeds in becoming a good alternative to Facebook.

« Newer PostsOlder Posts »

Powered by WordPress