Bookmark and Share

iPhone SDK Tutorial
Chapter 8. Navigation Controllers



8.5 Moveable - Fourth Subcontroller

In this section, we are going to implement a table whose rows can be moved, deleted, or a new row can be inserted. All these are done by turning on editing mode. This mode is using setEditing:animated: method on the table view. This method takes two Booleans. The first one indicates whether editing mode is on or off. The second Boolean indicates whether the table should animate the transition.

Once editing mode is on, a number of new delegate methods come into play. The table view uses them to ask if a certain row can be moved or edited.



The Move Me view controller can be implemented without a nib and with just a single controller class since we don't need to display a detail view. Name it "MoveMeController.m" after setting up with Cocoa Touch, Objective-C class, and NSObject for Subclass of.

Two things are needed for out header file. First, we need a mutable array hold our data and keep track of the order of the rows. We also need an action method to toggle edit mode. The action method will be called by a navigation bar button that we're going to make. Here is the interface file, "MoveMeController.h":

#import <UIKit/UIKit.h>
#import "SecondLevelViewController.h"

@interface MoveMeController : SecondLevelViewController {
    NSMutableArray *list;
}
@property (nonatomic, retain) NSMutableArray *list;
-(IBAction)toggleMove;
@end

Not, implementation file, "MoveMeController.m":

#import "MoveMeController.h"

@implementation MoveMeController
@synthesize list;

-(IBAction)toggleMove{
    [self.tableView setEditing:!self.tableView.editing animated:YES];
    
    if (self.tableView.editing)
        [self.navigationItem.rightBarButtonItem setTitle:@"Done"];
    else
        [self.navigationItem.rightBarButtonItem setTitle:@"Move"];
}

- (void)dealloc {
    [list release];
    [super dealloc];
}

- (void)viewDidLoad {
    if (list == nil)
    {
        NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:
                            @"Bjarne Stroustrup", @"Grady Booch", @"Dave Thomas", 
                            @"Michael Feathers", @"Ron Jeffries", @"Ward Cunningham", 
                            @"Joshua Bloch", @"Bruce Eckel", @"Scott Meyers", 
                            @"Mark Lutz", nil];
        self.list = array;
        [array release];        
    }
	
    UIBarButtonItem *moveButton = [[UIBarButtonItem alloc]
                                   initWithTitle:@"Move"
                                   style:UIBarButtonItemStyleBordered
                                   target:self
                                   action:@selector(toggleMove)];
    self.navigationItem.rightBarButtonItem = moveButton;
    [moveButton release];
    [super viewDidLoad];
}

#pragma mark -
#pragma mark Table Data Source Methods

- (NSInteger)tableView:(UITableView *)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return [list count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *MoveMeCellIdentifier = @"MoveMeCellIdentifier";
    
    UITableViewCell *cell = [tableView 
                        dequeueReusableCellWithIdentifier:MoveMeCellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                        reuseIdentifier:MoveMeCellIdentifier] autorelease];
        cell.showsReorderControl = YES;
        
    }
    NSUInteger row = [indexPath row];
    cell.textLabel.text = [list objectAtIndex:row];
    
    return cell;
}

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView 
           editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellEditingStyleNone;
}

- (BOOL)tableView:(UITableView *)tableView 
canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

- (void)tableView:(UITableView *)tableView 
moveRowAtIndexPath:(NSIndexPath *)fromIndexPath 
      toIndexPath:(NSIndexPath *)toIndexPath {
    NSUInteger fromRow = [fromIndexPath row];
    NSUInteger toRow = [toIndexPath row];
    
    id object = [[list objectAtIndex:fromRow] retain];
    [list removeObjectAtIndex:fromRow];
    [list insertObject:object atIndex:toRow];
    [object release];
}
@end

Let's look at our action method, toggleMove:

-(IBAction)toggleMove{
    [self.tableView setEditing:!self.tableView.editing animated:YES];
    
    if (self.tableView.editing)
        [self.navigationItem.rightBarButtonItem setTitle:@"Done"];
    else
        [self.navigationItem.rightBarButtonItem setTitle:@"Move"];
}

Here, we are toggleing edit mode and then setting the button's title to a proper value.

Next, we have a standard dealloc method, but no viewDidUnload method. We have no outlets, and if we were to flush out list array, we would lose any reordering that the use had done when the view gets flushed. We don't want that happening. So, we don't bother to override viewDidUnload.

The following method is viewDidLoad. It checks to see if list in empty, and if it is, it creates a mutable array, so our table has some data to show. The code after that requires some additional explanation:

    UIBarButtonItem *moveButton = [[UIBarButtonItem alloc]
                                   initWithTitle:@"Move"
                                   style:UIBarButtonItemStyleBordered
                                   target:self
                                   action:@selector(toggleMove)];
    self.navigationItem.rightBarButtonItem = moveButton;
    [moveButton release];

We are creating a button bar item, which is a button that will sit on the navigation bar. We give it a title of Move and then specify a constant, UIBarButtonItemStyleBordered, to indicate that we want a standard bordered bar button. The last two arguments, target and action, tell the button what to do when it is tapped. Notice we're passing self as the target and giving it a selector to the toggleMove method as the action. By doing this, we tell the button to call our toggleMov whenever the button is tapped. Then, we add it to the right side of the navigation bar, and then release it.

Let's look at the tableView:cellForRowAtIndexPath: method. Standard accessory icons can be specified by setting the accessoryType property of the cell. But, the reorder control is not a standard accessory icon. In other words, it's a special case that's shown only when the table is in edit mode. To enable the reorder control, we should set a property on the cell itself.

cell.showsReorderControl = YES;

However, setting this property to YES doesn't actually display the reorder control until the table gets put into edit mode.

In our table view, we want to be able to reorder the rows, but we don't want the user to be able to delete or insert rows. So, we implement the method tableView:editingStyleForRowAtIndexPath:. This method allows the table view to ask if a specific row can be deleted or if a new row can be inserted at a specific place. By returning UITableViewCellEditingStyleNone for each row, we are indicating that we don't support inserts or deletes for any row.

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView 
           editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    return UITableViewCellEditingStyleNone;
}

Next method is tableView: canMoveRowAtIndexPath:. This method is called for each row, and it gives us the chance to disallow the movement of specific rows. If we return NO from this method for any row, the reorder control will not be shown for that row, and the user will be unable to move it. We want to allow full reordering, so we just return YES for every row.

- (BOOL)tableView:(UITableView *)tableView 
canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

The final method, tableView: moveRowAtIndexPath:, is the method that will actually called when the user moves a row. The two parameters besides tableView are both NSIndexPath instances that identify the row that was moved and the row's new position. The table view has already moved so the user is seeing the right thing, be we need to update our data model to keep the two in sync and avoid display problems.

We retrieve the row that needs to be moved. After that, we retrieve the row's new position.

    NSUInteger fromRow = [fromIndexPath row];
    NSUInteger toRow = [toIndexPath row];

Now, it's time to remove the specified object from the array and reinsert it at its new position. But before we do that, we retrieve a pointer to the object to be moved and retain it so that the object doesn't get released when we remove it from the array. If the array is the only object that has retained the object we're removing, removing the selected object from the array will cause its retain count to drop0, meaning it will probably disappear on us. By retaining it first, we prevent that from happening.

    id object = [[list objectAtIndex:fromRow] retain];
    [list removeObjectAtIndex:fromRow];

After removing it, we should reinsert it into the specified new position. Then, we need to release it to avoid leaking memory since we've already retained it.

    [list insertObject:object atIndex:toRow];
    [object release];

What we've done. We've implemented a table that allows reordering of rows. So, all we need to to is adding an instance of this new class to FirstLevelViewController's array of controllers and adding an import statement.


#import "FirstLevelViewController.h"
#import "SecondLevelViewController.h"
#import "DisclosureButtonController.h"
#import "RowControlsController.h"
#import "CheckListController.h"
#import "MoveMeController.h"

@implementation FirstLevelViewController
@synthesize controllers;
- (void)viewDidLoad {
    self.title = @"First Level";
    NSMutableArray *array = [[NSMutableArray alloc] init];
	
    // Disclosure Button
    DisclosureButtonController *disclosureButtonController =
    [[DisclosureButtonController alloc] 
     initWithStyle:UITableViewStylePlain];
    disclosureButtonController.title = @"Disclosure Buttons";
    disclosureButtonController.rowImage = [UIImage 
                            imageNamed:@"disclosureButtonControllerIcon.png"];
    [array addObject:disclosureButtonController];
    [disclosureButtonController release];  
	
    // Check List 
    CheckListController *checkListController = [[CheckListController alloc]
                            initWithStyle:UITableViewStylePlain];
    checkListController.title = @"Check One";
    checkListController.rowImage = [UIImage imageNamed:
                            @"checkmarkControllerIcon.png"];
    [array addObject:checkListController];
    [checkListController release];

    // Table Row Controls
    RowControlsController *rowControlsController = 
    [[RowControlsController alloc] 
     initWithStyle:UITableViewStylePlain];
    rowControlsController.title = @"Row Controls";
    rowControlsController.rowImage = [UIImage imageNamed:
                            @"rowControlsIcon.png"];
    [array addObject:rowControlsController];
    [rowControlsController release];

    // Move Me
    MoveMeController *moveMeController = [[MoveMeController alloc]
                            initWithStyle:UITableViewStylePlain];
    moveMeController.title = @"Move Me";
    moveMeController.rowImage = [UIImage imageNamed:@"moveMeIcon.png"];
    [array addObject:moveMeController];
    [moveMeController release];
	
    self.controllers = array;
    [array release];
    [super viewDidLoad];
}

Build, Run, and Play




MoveMeResultA
MoveMeResultB MoveMeResultC



8.6 Deletable Rows - Fifth Subcontroller

Unlike the last section, we're going to load a property list file this time. Put the computers.plist into Resources folder.

Create a file with a name "DeleteMeController.m" from Cocoa Touch Class, Objective_C, and NSObject for Subclass of.

Here is the interface file, "DeleteMeController.h":

#import <UIKit/UIKit.h>
#import "SecondLevelViewController.h"

@interface DeleteMeController : SecondLevelViewController {
    NSMutableArray *list;
}
@property (nonatomic, retain) NSMutableArray *list;
-(IBAction)toggleEdit:(id)sender;
@end

Here we declared a mutable array to hold our data and an action method to toggle edit mode.

Implemention file, "DeleteMeController.m" is:

#import "DeleteMeController.h"

@implementation DeleteMeController
@synthesize list;

-(IBAction)toggleEdit:(id)sender {
    [self.tableView setEditing:!self.tableView.editing animated:YES];
    
    if (self.tableView.editing)
        [self.navigationItem.rightBarButtonItem setTitle:@"Done"];
    else
        [self.navigationItem.rightBarButtonItem setTitle:@"Delete"];
}

- (void)viewDidLoad {
    if (list == nil)
    {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"computers" 
                                                         ofType:@"plist"];
        NSMutableArray *array = [[NSMutableArray alloc] 
                                 initWithContentsOfFile:path];
        self.list = array;
        [array release];
    }
    UIBarButtonItem *editButton = [[UIBarButtonItem alloc]
                                   initWithTitle:@"Delete"
                                   style:UIBarButtonItemStyleBordered
                                   target:self
                                   action:@selector(toggleEdit:)];
    self.navigationItem.rightBarButtonItem = editButton;
    [editButton release];
    
    [super viewDidLoad];
}

- (void)dealloc {
    [list release];
    [super dealloc];
}

#pragma mark -
#pragma mark Table Data Source Methods

- (NSInteger)tableView:(UITableView *)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return [list count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *DeleteMeCellIdentifier = @"DeleteMeCellIdentifier";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
                             DeleteMeCellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                            reuseIdentifier:DeleteMeCellIdentifier] autorelease];
    }
    NSInteger row = [indexPath row];
    cell.textLabel.text = [self.list objectAtIndex:row];
    return cell;
}

#pragma mark -
#pragma mark Table View Data Source Methods

- (void)tableView:(UITableView *)tableView 
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle 
forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSUInteger row = [indexPath row];
    [self.list removeObjectAtIndex:row];
    [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
                     withRowAnimation:UITableViewRowAnimationFade];
}
@end

The action method, toggleEdit:, sets edit mode to on if it's currently off and vice versa. Then, it sets the button's title as appropriate, either "Done" or "Delete".

    if (self.tableView.editing)
        [self.navigationItem.rightBarButtonItem setTitle:@"Done"];
    else
        [self.navigationItem.rightBarButtonItem setTitle:@"Delete"];

In the viewDidLoad method, we're loading our array from a property list rather than as a hard-coded list of strings. The property list we're using is a flat array of strings containing a various computer model name. We also assign a different name to the edit button this time, naming it Delete to make the button's effect obvious to the user.

    if (list == nil)
    {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"computers" 
                                                         ofType:@"plist"];
        NSMutableArray *array = [[NSMutableArray alloc] 
                                 initWithContentsOfFile:path];
        self.list = array;
        [array release];
    }
    UIBarButtonItem *editButton = [[UIBarButtonItem alloc]
                                   initWithTitle:@"Delete"
                                   style:UIBarButtonItemStyleBordered
                                   target:self
                                   action:@selector(toggleEdit:)];

The second data source method,
tableView: commitEditingStyle: forRowAtIndexPath: worth closer look. This method is called by the table view when the user has finished an edit. That means a delete or an insert. The first argument is the table view on which a row was edited. The second parameter, editingStyle, is a constant that telling us what kind of edit just happened. There are three editing styles defined. One of them is UITableViewCellEditingStyleNone, which we used in the last section to indicate that a row can't be edited. The other two styles are UITableViewCellEditingStyleDelete, which is the default option, and UITableViewCellEditingStyleInsert. The option UITableViewCellEditingStyleNone will never be passed into this method. That's because it is used to indicate that editing is not allowed for this row.

We ignore this parameter, because the default editing style for rows is the delete style, so we know that whenever this method gets called, it will be requesting a delete.

The last parameter, indexPath, telling us which row is being edited. For a delete, this index path indicates the row to be deleted. In our method, we first retrieve the row that is being edited from indexPath:

    NSUInteger row = [indexPath row];

Then, we remove the object from the mutable array:

    [self.list removeObjectAtIndex:row];

In the last line of the method, we tell the table to delete the row, specifying the constant UITableViewRowAnimationFade, which is one type of animation when removing rows.

    [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] 
            withRowAnimation:UITableViewRowAnimationFade];

Final steps are the same as the last section. Add an instance of this class to the "FirstLevelViewController.m" and we need to insert a header of DeleteMeController's class.



#import "FirstLevelViewController.h"
#import "SecondLevelViewController.h"
#import "DisclosureButtonController.h"
#import "RowControlsController.h"
#import "CheckListController.h"
#import "MoveMeController.h"
#import "DeleteMeController.h"

@implementation FirstLevelViewController
@synthesize controllers;
- (void)viewDidLoad {
    self.title = @"First Level";
    NSMutableArray *array = [[NSMutableArray alloc] init];
	
    // Disclosure Button
    DisclosureButtonController *disclosureButtonController =
    [[DisclosureButtonController alloc] 
     initWithStyle:UITableViewStylePlain];
    disclosureButtonController.title = @"Disclosure Buttons";
    disclosureButtonController.rowImage = [UIImage 
                                imageNamed:@"disclosureButtonControllerIcon.png"];
    [array addObject:disclosureButtonController];
    [disclosureButtonController release];  
	
    // Check List 
    CheckListController *checkListController = [[CheckListController alloc]
                                initWithStyle:UITableViewStylePlain];
    checkListController.title = @"Check One";
    checkListController.rowImage = [UIImage imageNamed:
                                @"checkmarkControllerIcon.png"];
    [array addObject:checkListController];
    [checkListController release];

    // Table Row Controls
    RowControlsController *rowControlsController = 
    [[RowControlsController alloc] 
     initWithStyle:UITableViewStylePlain];
    rowControlsController.title = @"Row Controls";
    rowControlsController.rowImage = [UIImage imageNamed:
                                @"rowControlsIcon.png"];
    [array addObject:rowControlsController];
    [rowControlsController release];

    // Move Me
    MoveMeController *moveMeController = [[MoveMeController alloc]
                            initWithStyle:UITableViewStylePlain];
    moveMeController.title = @"Move Me";
    moveMeController.rowImage = [UIImage imageNamed:@"moveMeIcon.png"];
    [array addObject:moveMeController];
    [moveMeController release];
	
    // Delete Me
    DeleteMeController *deleteMeController = [[DeleteMeController alloc] 
                            initWithStyle:UITableViewStylePlain];
    deleteMeController.title = @"Delete Me";
    deleteMeController.rowImage = [UIImage imageNamed:@"deleteMeIcon.png"];
    [array addObject:deleteMeController];
    [deleteMeController release];
	
    self.controllers = array;
    [array release];
    [super viewDidLoad];
}

- (void)viewDidUnload {
    self.controllers = nil;
    [super viewDidUnload];
}

- (void)dealloc {
    [controllers release];
    [super dealloc];
}


Time to Run and Play.



DeleteMeResultA

DeleteMeResultB DeleteMeResultC

DeleteMeResultD DeleteMeResultE




8.7 Editable Detail Pane - Sixth Subcontroller

This section will explore how to implement a reusable editable detail view.

I will come back later for this section.



Previous Sections.