2011年11月3日星期四

Using Blocks with Table Views

Using Blocks with Table Views:

Anyone who has implemented a table view has encountered this tricky problem: when you have a master to detail relationship when you leave table view to go to an editing detail screen the changes that the user makes are not reflected in the table view. So, when the user changes the content in a detail view and then goes back to the master table view it looks like something went wrong and the changes were lost.

As you probably know, the changes are not lost but the UI in iOS doesn’t get updated automatically because the screens are cached in the navigation controller. This is an annoying situation and up till now to fix this you had to make a choice between reloading the entire table view (very inefficient) or implementing a tricky custom delegation pattern (better but tons of code). But now, if you are willing to stretch your skills just a little bit and learn a little about blocks you can solve this problem more easily and efficiently.

Set Up the Situation


Let’s assume that our app maintains a list of names which is presented to the user via a table view. Users may select rows which will take them to a editing screen where the users can change the name. Your first instinct when making sometime like this would be to create a table view controller (for the list of names) and a detail view controller (for the editing screen).

Your code would probably look something like this:

Table View Controller Code (first attempt)


#import "Table.h"

@implementation Table
NSMutableArray *listOfStrings;

-(void)viewDidLoad{
   [super viewDidLoad];
   listOfStrings = [[NSMutableArray alloc] init];
   [listOfStrings addObject:@"A"];
   [listOfStrings addObject:@"B"];
   [listOfStrings addObject:@"C"];
}

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
   static NSString *CellIdentifier = @"Cell";

   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
   if (cell == nil) {
       cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
   }

   cell.textLabel.text = [listOfStrings objectAtIndex:indexPath.row];

   return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
   DetailView *detailViewController = [[DetailView alloc] initWithNibName:@"DetailView" bundle:nil];

   detailViewController.string = [listOfStrings objectAtIndex:indexPath.row];

   [self.navigationController pushViewController:detailViewController animated:YES];
}

@end

Detail View Controller (first attempt)


#import "DetailView.h"

@implementation DetailView
@synthesize string;
@synthesize myTextField;

- (void)viewDidLoad{
   [super viewDidLoad];
   self.myTextField.text = self.string;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
   if([self.string isEqualToString:textField.text] == NO){
       self.string = textField.text;
   }
   [textField resignFirstResponder];

   return YES;
}

- (void)viewDidUnload {
   [self setMyTextField:nil];
   [super viewDidUnload];
}

@end

That’s the easiest way to implement a table and detail view app. This is the implementation that cause the problem I mentioned at the start though – users can change the model class but the content is not updated in the presentation.

Just Reloading All Data


Most people simple fix this problem by implementing the viewWillAppear delegate method and reloading all the table view rows. This is pretty inefficient and most Apple engineers I’ve talked to about this warn against it. But, here is who you would do this:

-(void)viewWillAppear:(BOOL)animated{
   [super viewWillAppear:animated];
   [self.tableView reloadData];
}

Implementing Delegation


A more elegant way to fix this problem is to implement Delegation with your detail view controller. This will take you a few steps to get done:


  • Define a protocol

  • Add a delegate property that requires the protocol to your detail view controller

  • Have your master class (the table view) adopt the protocol

  • Make sure your master class implements the protocol methods

  • Make sure the detail class sends key messages to the delegate

  • Update the master UI in response to the delegate messages you receive

So, that’s a lot of work and would require a whole other article to show you know to do it. Luckily, there is an easier way.

Blocks


Finally, there is an easier way with the inclusion of blocks in Objective-C 2.0. Blocks are literally blocks of code that you can reference as an object. Blocks remember the state of the code around them so you can declare a block and then send the block to another object to execute at some point in the future and the block will remember everything that was happening at the time the block was reached (not when it was executed).

So, here is what this means for our little problem: when we set the detail view controller’s model content we can also pass along the code we want to execute that will update the UI. This code will be frozen and saved for later and the detail view controller can execute this block if and when the content needs to be updated. It sorta sounds a little bit weird and spooky if you are not used to this type of programming right?

So, here is how you would do this (I’ll highlight all the parts involving blocks). First, let’s start with the detail view controller:

#import "DetailView.h"

@implementation DetailView
@synthesize string;
@synthesize myTextField;

void (^blockToExecute)(NSString* string);

- (void)viewDidLoad{
   [super viewDidLoad];
   self.myTextField.text = self.string;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField{
   if([self.string isEqualToString:textField.text] == NO){
       self.string = textField.text;
       blockToExecute(self.string);
   }
   [textField resignFirstResponder];

   return YES;
}

- (void)viewDidUnload {
   [self setMyTextField:nil];
   [super viewDidUnload];
}

-(void)setThisModel:(NSString *)newString withThisBlock:(void (^)(NSString* string)) block{
   self.string = newString;
   blockToExecute = block;
}

@end

We are asking not only for the model content to display but also some code to execute when the user makes a change. Now here is how we can do this with the table view controller (master):

#import "Table.h"

@implementation Table
NSMutableArray *listOfStrings;

-(void)viewDidLoad{
   [super viewDidLoad];
   listOfStrings = [[NSMutableArray alloc] init];
   [listOfStrings addObject:@"A"];
   [listOfStrings addObject:@"B"];
   [listOfStrings addObject:@"C"];
}

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
   static NSString *CellIdentifier = @"Cell";

   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
   if (cell == nil) {
       cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
   }

   cell.textLabel.text = [listOfStrings objectAtIndex:indexPath.row];

   return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
   DetailView *detailViewController = [[DetailView alloc] initWithNibName:@"DetailView" bundle:nil];

   [detailViewController setThisModel:[listOfStrings objectAtIndex:indexPath.row]
                        withThisBlock:^(NSString* string){
                            [listOfStrings replaceObjectAtIndex:indexPath.row
                                                     withObject:string];
                            UITableViewCell *tvc = [tableView cellForRowAtIndexPath:indexPath];
                            tvc.textLabel.text = string;
                        }];

   [self.navigationController pushViewController:detailViewController animated:YES];
}

@end

Pretty tricky huh?

没有评论:

发表评论