Profile photo of Alex

by

Updating Data in iOS Client Applications

October 26, 2011 in Cocoa, Data Abstract, iOS, Mac, Relativity, Uncategorized, Xcode

As promised earlier, I’m going to tell you how to change data in an iOS application and apply changes to the DA Server. As client application we will use the iOS Relativity Client we have previously created.

In the course of this session we will try to implement an editor for changing data from the Orders table. We will also use UIPickerView & UIDatePicker for changing the Order’s fields.
The source code can be obtained from here.
Let’s start!

The first thing we need to do is to add a new editor view and its controller.
After performing File->New…->New File, select the “UIViewController subclass” item. As the base class we will use the UITableViewController.
Now we need to add an edit button to the existing OrdersViewController. Clicking on this button should open an editor for the selected row.

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // create & set Edit button    
    UIBarButtonItem *actionButton = 
     [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit 
                                                   target:self 
                                                   action:@selector(edit:)];
    [[self navigationItem] setRightBarButtonItem:actionButton];
    [actionButton release];
    ...
}
 
- (IBAction)edit:(id)aSender {
 
    OrderEditorController *editor = 
      [[[OrderEditorController alloc] initWithStyle:UITableViewStyleGrouped] autorelease];
 
    // set OrderViewController as the delegate to be able to notify it about the results of the editing process
    [editor setDelegate:self];
 
    // find the currently selected row
    NSIndexPath *path = [self.tableView indexPathForSelectedRow];
    NSInteger idx = [path indexAtPosition:1];
    DADataTableRow *row = [orderRows objectAtIndex:idx];
 
    // and set it for our editor 
    [editor setOrderRow:row];
 
    // push the editor view to be a topmost view
    [[self navigationController] pushViewController:editor animated:YES];
}
 
// delegate the method that the editor will call for applying changes
-(void)endEditWithOk:(DADataTableRow *)row {
    // applying changes on Relativity server
    [[DataAccess sharedInstance] applyUpdatesForTable:[row table]];
    // reload view for synchronizing client-side data
    [[self tableView] reloadData];
}
 
// delegate the method that the editor will call for canceling changes
-(void)endEditWithCancel:(DADataTableRow *)row {
    // cancel the change
    [row cancel];
    // reload table
    [[self tableView] reloadData];
}

Now it is time to start implementing our OrderEditorController. The Orders table has several fields we can edit: OrderDate and OrderStatus, but we can also change the client of the order, though I don’t think we need to change the document in such a radical way ;) .

Note: There is one small problem we need to resolve first. Unfortunately, the current version of the PCTrade database doesn’t have an OrderStatus reference, so the Orders table only has its integer values (which by default are 0).
Let’s assume that we need to implement several available document statuses. A new uncompleted order usually has the “New” status. When it is completed but not payed yet, it has a “Pending” status. When we received the money, it changes to “Closed” and finally, there is the additional “Deleted” status. In the future we probably may want to track the document history, so we loose any of the orders, even if they were deleted. So our database will keep those orders, but with the “Deleted” status. In order to keep the correspondence between the available status numbers and their names, we will add a new NSArray into our DataAccess class as shown below.

/////////////////////////////
//  DataAccess.h
@interface DataAccess : NSObject <DARemoteDataAdapterDelegate, DAAsyncRequestDelegate>
{
    ...
    NSArray *orderStatusNames;
}
...
@property (readonly) NSArray *orderStatusNames;
-(NSUInteger)idForOrderStatusName:(NSString *)name;
-(NSString *)nameForOrderStatus:(NSUInteger)status;
@end
 
/////////////////////////////
//  DataAccess.m
 
- (id) init {
	self = [super init];
	if (self) {
		...
 
        orderStatusNames = [[NSArray alloc] initWithObjects:
                         @"New", 
                         @"Pending", 
                         @"Closed", 
                         @"Deleted", 
                         nil];
 
	}
	return self;
}
 
- (void) dealloc {
   ...  
   [orderStatusNames release], orderStatusNames = nil;
   [super dealloc];
}
 
 
-(NSString *)nameForOrderStatus:(NSUInteger)status {
 
    if (status >= [orderStatusNames count])
        return @"Unknown state";
 
    return [orderStatusNames objectAtIndex:status];
}
 
-(NSUInteger)idForOrderStatusName:(NSString *)name { 
 
    for (NSUInteger i = 0; i < [orderStatusNames count]; i++) {
        NSString *statusName = [orderStatusNames objectAtIndex:i];
        if ([statusName compare:name] == NSOrderedSame) 
            return i;
    }
    return 0;
}
 
-(NSArray *)orderStatusNames {
 
    return orderStatusNames;
}

Now we are ready to start the editor implementation. Please open the OrderEditorController.h file and add the following:

@interface OrderEditorController : UITableViewController {
 
    DADataTableRow *orderRow;
 
    UIBarButtonItem *saveButton;
    UIBarButtonItem *cancelButton;
 
    id delegate;
    NSDateFormatter *df; 
}
 
@property (retain) DADataTableRow *orderRow;
@property (assign) id delegate;
 
- (IBAction)save:(id)aSender;
- (IBAction)cancel:(id)aSender;
@end

Our editor should have references to the edited row, the delegate reference, the save & cancel buttons and the appropriate methods. This will implement them:

#pragma mark - View lifecycle
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    // create & add cancel button    
    cancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" 
                                                    style:UIBarButtonItemStylePlain 
                                                   target:self action:@selector(cancel:)];
    self.navigationItem.leftBarButtonItem = cancelButton;
 
    // create & add save button    
    saveButton = [[UIBarButtonItem alloc] initWithTitle:@"Save" 
                                                  style:UIBarButtonItemStyleDone 
                                                 target:self action:@selector(save:)];
    self.navigationItem.rightBarButtonItem = saveButton;
 
    // we will also need this formatter for converting NSDate to NSString 
    df = [[NSDateFormatter alloc] init];
    [df setDateStyle:NSDateFormatterMediumStyle];
}
 
- (IBAction)save:(id)aSender {
 
    if ([delegate respondsToSelector:@selector(endEditWithOk:)])
        [delegate endEditWithOk:orderRow];
 
    [self.navigationController popViewControllerAnimated:YES];
 
}
 
- (IBAction)cancel:(id)aSender {
 
    if ([delegate respondsToSelector:@selector(endEditWithCancel:)])
        [delegate endEditWithCancel:orderRow];
 
    [self.navigationController popViewControllerAnimated:YES];
}

In the majority of cases, we can represent the editor view as the table view, where each cell represents the field name at the left side and the current value at the right side. We can also group certain fields together based on general meaning or application logic. To do that we have to initialize our OrderEditorController with the UITableViewStyleGrouped style. When the user taps on a certain cell, we will need to allow him to edit that value.

So now we need to configure our table view as shown below:

 
#pragma mark - Table view data source
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // we want to have two groups, 
    // one for OrderDate & OrderStatus, and one for the Client field 
    return 2;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    switch (section) {
        case 0:
            // the first section will have two cells (showing OrderDate & OrderStatus)
            return 2;
        case 1:
            // the second section will have a single cell (showing the Client field)
            return 1;
 
        default:
            return 0;
    }
}
 
static NSString *TABLE_CELL_ID = @"OrderEditorCell";
 
- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TABLE_CELL_ID];
    if (cell == nil) {
      cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 
                                     reuseIdentifier:TABLE_CELL_ID] autorelease];
    }
 
    // Configure the cell...
    int section = [indexPath section];
    int index = [indexPath indexAtPosition:1];
    switch (section) {
      case 0: { // for the first section ...
        switch (index) {
          case 0: { // configure the cell to show the OrderDate field
                    [[cell textLabel] setText:@"Order Date:"];
                    NSDate *value = [orderRow valueForKey:@"OrderDate"];
                    NSString *valueAsString = value ? [df stringFromDate:value] : @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
          case 1: { // configure the cell to show the OrderStatus field
                    [[cell textLabel] setText:@"Order Status:"];
                    NSNumber *value = [orderRow valueForKey:@"OrderStatus"];
                    NSString *valueAsString = value ? 
                         [[DataAccess sharedInstance] nameForOrderStatus:[value intValue]] : 
                         @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
        }
        break;
      }
      case 1: { // for the second section
        switch (index) {
          case 0: { // configure the cell to show the Client field
                    [[cell textLabel] setText:@"Client:"];
                    NSString *value = [orderRow valueForKey:@"CustomerName"];
                    NSString *valueAsString = value ? value : @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
        }
        break;
     }
     default:
       break;
    }
    [cell setNeedsDisplay];
	return cell;
}

Now, when you compile and run the project, you should see something like the following:

Next we can start to implement the editing for the OrderDate and OrderStatus fields. For editing dates we can use the really good NSDatePicker control with the appropriate configuration. For editing the OrderStatus we can use our custom picker populated with available statuses.
I also want the picker view to appear at the bottom of the current editor using animation, like the standard Contacts application does. Having spent some time with Google, I’ve found an interesting Data Cell sample with the required functionality. I’ve just slightly adjusted it for our purposes, namely to support animation for custom pickers.

- (void)viewDidLoad {
 
  ...
  // create & configure our DatePicker
  datePickerView = [[UIDatePicker alloc] init];
  [datePickerView setDatePickerMode:UIDatePickerModeDate];
  [datePickerView addTarget:self 
                     action:@selector(pickerDidChange) 
           forControlEvents:UIControlEventValueChanged];
 
  pickerDoneButton = [[UIBarButtonItem alloc] initWithTitle:@"Done" 
                                                      style:UIBarButtonItemStyleDone 
                                                     target:self action:@selector(pickerDone)];
  pickerCancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" 
                                                        style:UIBarButtonItemStyleDone 
                                                       target:self action:@selector(pickerCancel)];
}
 
#pragma mark - Common picker routines
-(void)showPicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    if (activePicker) 
        [self hidePicker:activePicker];
 
    activePicker = picker;
 
    if (picker.superview == nil) {
        [self.view.window addSubview:picker];
 
        // size up the picker view to our screen and compute the start/end 
        // frame the origin for our slide-up animation
        // compute the start frame
        CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
        CGSize pickerSize = [picker sizeThatFits:CGSizeZero];
        CGRect startRect = CGRectMake(0.0,
                                      screenRect.origin.y + screenRect.size.height,
                                      pickerSize.width, pickerSize.height);
        picker.frame = startRect;
 
        // compute the end frame
        CGRect pickerRect = CGRectMake(0.0,
                                       screenRect.origin.y + screenRect.size.height - pickerSize.height,
                                       pickerSize.width,
                                       pickerSize.height);
        // start the slide-up animation
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:0.3];
 
        // we need to perform some post operations after the animation is complete
        [UIView setAnimationDelegate:self];
 
        picker.frame = pickerRect;
 
        // shrink the table's vertical size to make room for the date picker
        CGRect newFrame = self.tableView.frame;
        newFrame.size.height -= picker.frame.size.height;
        self.tableView.frame = newFrame;
        [UIView commitAnimations];
 
        // add the "Done" & "Cancel" buttons to the nav bar
        self.navigationItem.rightBarButtonItem = pickerDoneButton;
        self.navigationItem.leftBarButtonItem = pickerCancelButton;
    }
}
 
-(void)removePicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    if (picker.superview != nil) {
        [picker removeFromSuperview];    
    }
}
 
-(void)hidePicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    activePicker = nil;
 
    CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
    CGRect endFrame = picker.frame;
    endFrame.origin.y = screenRect.origin.y + screenRect.size.height;
 
    // start the slide-down animation
    [UIView beginAnimations:nil context:picker];
    [UIView setAnimationDuration:0.3];
 
    // we need to perform some post operations after the animation is complete
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
 
    picker.frame = endFrame;
    [UIView commitAnimations];
 
    // grow the table back again in vertical size to make room for the date picker
    CGRect newFrame = self.tableView.frame;
    newFrame.size.height += picker.frame.size.height;
    self.tableView.frame = newFrame;
 
    // remove the "Done" button and restore the nav bar
    self.navigationItem.rightBarButtonItem = saveButton;
    self.navigationItem.leftBarButtonItem = cancelButton;
 
    // deselect the current table row
    //NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
    //[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
 
}
 
-(void)animationDidStop:(NSString *)animationID 
               finished:(NSNumber *)finished 
                context:(void *)context {
    [self removePicker:context];
}
 
// called when we click the Done button
-(void)pickerDone {
 
    // change the data
    if (activePicker == (UIPickerView *)datePickerView)
        [orderRow setValue:datePickerView.date forKey:@"OrderDate"];
 
    // hide picker
    [self hidePicker:activePicker];
    // reload table view to show changes
    [[self tableView] reloadData];
}
 
// called when we click on the Done button
-(void)pickerCancel {
    // just hide the picker
    [self hidePicker: activePicker];
    [[self tableView] reloadData];
}
 
// the following method will sync the current picker value with the value in the table view
- (void)pickerDidChange {
 
    NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
 
    if (activePicker == (UIPickerView *)datePickerView)
        cell.detailTextLabel.text = [df stringFromDate:datePickerView.date];
}

Now we need to launch the UIDatePicker when a user taps on the OrderDate field:

- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    [self hidePicker:activePicker];
    int section = [indexPath section];
    int index = [indexPath indexAtPosition:1];
 
    switch (section) {
      case 0: {
        switch (index) {
          case 0: {
            datePickerView.date = [orderRow valueForKey:@"OrderDate"];
            [self showPicker:(UIPickerView *)datePickerView];
            break;
          }
          case 1: {
 
            break;
	  }
        }
      }
    }
}

When you compile and run the application, you should get something like the following:

Finally, we need to implement our custom UIPickerView, which is quite simple. At the beginning we need to create a custom UIPickerView and set its data source and delegate properties:

- (void)viewDidLoad {
    ...    
    statePickerView = [[UIPickerView alloc] init];
    [statePickerView setDataSource:self];
    [statePickerView setDelegate:self];
    [statePickerView setShowsSelectionIndicator:YES];
}

Then we need to add a support UIPickerViewDataSource protocol to our editor class and implement several methods, as shown below:

 
#pragma mark - Custom Picker routines
 
// callback method for filling content for our custom statuses picker
- (NSString *)pickerView:(UIPickerView *)pickerView 
             titleForRow:(NSInteger)row 
            forComponent:(NSInteger)component {
 
    if (pickerView == statePickerView)
        return [[DataAccess sharedInstance] nameForOrderStatus:row];
    return @"";
}
 
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
 
    // it should be a simple picker with a single component
    if (pickerView == statePickerView)
        return 1;
    return 0;
}
 
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
 
    if (pickerView == statePickerView)
        return [[[DataAccess sharedInstance] orderStatusNames] count];
    return 0;
}

Change the tableView:didSelectRowAtIndexPath: method to show our state picker:

#pragma mark - Table view delegate
 
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   ...    
    switch (section) {
      case 0: {
        switch (index) {
          case 0: {
            ...
          }
          case 1: {
            [self showPicker:statePickerView];
            break;
          }
        }
      }
    }
}

That’s all. Now when you tap on the status cell of the editor, you should get something like this:

You can choose a new status, then click the done button and finally save the current order.

I’m slightly worried that you may find this blog post a little confusing and not easy to reproduce. We wrote quite a lot of code here, and I deliberately skipped some minor details, trying to make it easier, but you can download the whole source code from here and play with the project yourself.

Thank you for your attention ;)

Comments are closed.