iPhone开发基础教程笔记(七)--第八章 表视图简介

第八章  表视图简介

表视图是用于向用户显示数据的一种最常见的机制。他们是高度可配置的对象,可以被配置为用户所需的任何形式。
电子邮件使用表视图显示账户、文件夹和消息的列表,但是表视图并不是仅限于显示文本数据。还可以在YouTube、Settings和iPod

应用程序中使用表视图,尽管这些应用程序具有十分不同的外观。

8.1 表视图基础
表用于显示数据列表。数据列表中的每项都由行表示。iPhone表没有限制行的数量,其数量仅受可用存储空间的限制。iPhone表可

以只有一列。
表视图是显示表数据的视图对象,他是UITableView类的一个实例。表中的每个可见行都有UITableViewCell类实现。因此,表视图

是显示表中可见部分的对象,表视图单元负责显示表中的一行。
表视图并不负责存储表中的数据。他们只存储足够绘制当前可见行的数据。表视图从遵循UITableViewDelegate协议的对象获取配置

数据,从遵循UITableViewDatasource协议的对象获取行数据。
表中的每一行都由一个UITableViewCell表示,可以使用一个图像、一些文本和一个可选的辅助图标来配置每个UITableViewCell对

象。
如果需要的话,可以在一个单元中放置更多的数据。可以通过两种基本方法来完成此操作:
1)向UITableViewCell添加子视图
2)通过子类化UITableViewCell。

分组表和索引表
表视图分为两种基本样式:分组表和索引表
1)分组表:分组表中的每个组都由嵌入在圆角矩形中的多个行组成。注意,一个分组表可以只包含一个组。
2)索引表(在某些地方又称为无格式表):索引表是默认的样式。任何没有圆角矩形属性的表都是索引表视图。

表中的每个部分被称为数据源中的“分区”(section)。在分组表中,每个分组都是一个分区。在索引表中,数据的每个索引分组

都是一个分区。
分区主要有两个作用:在分组表中,每个分区表示一个组。在索引表中,每个分区对应一个索引条目。因此,如果你希望显示一个

按字母顺序列出索引且每个字母做为一个索引条目的列表,那么你将拥有26个分区,每个分区包含以特定字母开头的所有值。

注意:从技术上来说,可以创建带有索引的分组表。即便如此,也不应该为分组表视图提供索引。

8.2 实现一个简单的表
下面通过一个最简单的示例来了解表视图的工作原理。本示例将显示一个文本值列表。
在Xcode中创建一个新项目。使用基于视图的应用程序模版。命名为Simple Table。

8.2.1 设计视图
展开Resources和Classes文件夹。这是一个极为简单的应用程序,他不需要任何输出口或操作。
打开Simple_TableViewController.xib,从库中拖出一个Table View,将他放置到View窗口中即可。
表视图会自动将其高度和宽度调整为View窗口的高度和宽度。
选中表视图,按Cmd+2打开连接检查器。设置其数据源和委托为File's Owner。

8.2.2 编写控制器
遵循 <UITableViewDelegate,UITableViewDataSource>,并添加
NSArray *listData;实例变量

- (void)viewDidLoad {
 NSArray *array=[[NSArray alloc] initWithObjects:@"Sleepy",@"Sneezy",
  @"Bashful",@"Happy",...,@"Bombur",nil];
 self.listData=array;
 [array release];
 [super viewDidLoad];
}

#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{ return [self.listData count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *SimpleTableIdentifier=@"SimpleTableIdentifier";
  UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:SimpleTableInentifier];
  if (cell==nil) {
   cell=[[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:SimpleTableIdentifier] autorelease];
  }
  NSUInteger row=[indexPath row];
  cell.text=[listData objectAtIndex:row];
  return cell;
}

解析:
两个Datasource方法
第一个默认的分区数量为1,此方法用于返回组成列表的表分区中的行数。只需返回数组中数组项的数量即可。
下一个方法可能需要一些解释。让我们更仔细地看一下此方法。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
当表视图需要绘制其中一行时,则会调用此方法。你会注意到此方法的第二个参数是一个i额NSIndexPath实例。表视图证实使用此

机制来分区和行绑定到一个对象中的。要从NSIndexPath中获得一行或一个分区,只需要调用行方法或分区方法就可以了。这两个方

法都返回一个int值。
下面声明一个静态字符串实例:
{ static NSString *SimpleTableIdentifier=@"SimpleTableIdentifier";
此字符串充当表示某种表单元的键。在此表中,我们将只使用一种单元,因此定义一种标识符就可以了。表视图在iPhone的小屏幕

上一次只能显示几行,但是表自身能够保存相当多的数据。记住,表中的每一行都由一个UITableViewCell实例表示,该实例是一个

UIView的子类,这就意味着每一行都能拥有子视图。对于大型表来说,如果视图为表中的每一行都分配一个表视图单元,不管该行

当前是否正被显示,这都将带来大量开销。幸好表并不是这样工作的。
相反,因滚动操作离开屏幕的一些表视图单元,将被放置在一个可以被重用的单元序列中。如果系统运行比较慢,表视图就从序列

中删除这些单元,以释放存储空间。不过,只要有可用的存储空间,表视图就会重新获取这些单元,以便以后再次使用他们。
当一个表视图单元滚出屏幕时,另一个表视图单元就会从另一边滚动到屏幕上。如果滚动到屏幕上的新行重新使用从屏幕上滚动下

来的其中一个单元,系统就会避免与不断创建和释放那些视图相关的开销。要充分利用此机制,我们需要让表视图给定一个出列单

元,该出列单元正是我们需要的类型。注意,我们现在正在使用前面声明的NSString标识符。实际上,我们需要一个

SimpleTableIdentifier类型的可重用单元:(dequeue=出列,Reusable=可重用)
  UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:SimpleTableInentifier];
现在,表视图中可能没有任何多余的单元了,我们来检查这些cell,看一下他是否为nil。如果是,则使用上面提到的标识符字符串

创建一个新的表视图单元。从某种程度上来说,我们将不可避免地重复使用此处创建的单元,因此需要确保他具有相同的类型:
  if (cell==nil) {
   cell=[[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:SimpleTableIdentifier] autorelease];
  }
现在,我们拥有了一个可以返回到表视图的表视图单元。下面所有要做的就是把需要显示的信息放在该表视图单元中。在表的一行

内显示文本是很常见的任务,因此表视图通过了一个名为text的属性。我们可以设置此属性以显示字符串。
要完成上述操作,需要知道表视图需要显示哪些行。可以从indexPath变量获取该信息。
  NSUInteger row=[indexPath row];
  cell.text=[listData objectAtIndex:row];
  return cell;
}
现在编译运行程序,出现了一个链接错误?哪里错了?提示:我们使用了一个名为CGRectZero的常量,他是Core Graphics框架的一

部分。你如果忘记了如何将它链接进来的话,可以参见第五章。

8.3 添加一个图像
我们需要创建一个UITableViewCell子类来添加图像吗?不用,实际上,如果能够让图像位于每一行的左侧就不需要这么做了。默认

的表视图单元会把这个情况处理好。
在项目归档文件08 Simple Table文件夹中,找到start.png文件,然后把他添加到Resources文件夹。
下面看看代码部分。tableView:cellForRowAtIndexPath:方法

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *SimpleTableIdentifier=@"SimpleTableIdentifier";
  UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:SimpleTableInentifier];
  if (cell==nil) {
   cell=[[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:SimpleTableIdentifier] autorelease];
  }
  NSUInteger row=[indexPath row];
  cell.text=[listData objectAtIndex:row];
  UIImage *image=[UIImage imageNamed:@"start.png"];
  cell.image=image;
  return cell;
}


8.4 附加配置
表视图的委托只是用于配置表视图的外观并处理某些用户交互。
8.4.1 设置缩进级别
可以使用委托指定缩进某些行
#pragma mark -
#pragma mark Table Delegate Methods
- (NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath
{ NSUInteger row=[indexPath row];
 return row;
}
此方法把每一行的缩进级别设置为其行号。

8.4.2 处理行的选择
表的委托可以使用两个方法确定用户是否选择了特定的行。一个方法在一行被突出显示之前调用,并且可以阻止选中此行,甚至改

变被选中的行。

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{ NSUInteger row=[indexPath row];
 if (row==0)
   return nil;
 return indexPath;
}
return nil表示实际上没有行被选中,否则,他返回indexPath,表示选择可以继续进行。

还有一个方法是在一行被选中之后调用。通常他也是实际处理选择的地方。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{ NSUInteger row=[indexPath row];
 NSString *rowValue=[listData objectAtIndex:row];
 NSString *message=[[NSString alloc] initWithFormat:@"You selected %@",rowValue];
 UIAlertView *alert=[[UIAlertView alloc] initWithTitle:@"Row Selected!"
  message:message
  delegate:nil
  cancelButtonTitle:@"Yes I Did"
  otherButtonTitles:nil];
 [alert show];
 [message release];
 [alert release];
}

注意,你还可以在传递会indexPath之前修改索引路径,这将导致不同的行和/或分区被选中。不过你一般不会这么做,即只返回

indexPath或nil。

8.4.3 更改字体大小或行高
tableView:cellForRowAtIndexPath:方法中增加:
cell.font=[UIFont boldSystemFontOfSize:80];
现在,列表中的值将会变得很大,但是他们的大小并不适合行。
好,现在靠表视图委托来救援吧!表视图委托可以指定表中的行高。实际上,如果需要的话,他可以为每一行指定惟一值。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
 return 180;
}

8.4.4 委托还能做什么?
上面介绍了几个委托方法:
tableView:indentationLevelForRowAtIndexPath:设置缩进级别
tableView:heightForRowAtIndexPath: 设置行高
tableView:willSelectRowAtIndexPath: 选择行之前
tableView:didSelectRowAtIndexPath: 选择行之后
要了解更多内容,请使用文档浏览器查看UITextViewDelegate协议。

8.5 定制表视图单元
向UITableViewCell添加子视图 or  创建UITableViewCell子类

8.5.1 单元应用程序
要展示如何使用自定义单元,我们将创建一个新的应用程序,使用另一个表视图,然后向用户显示两行信息。应用程序将显示一系

列常见计算机模型的名称和颜色。通过向表视图单元添加子视图,我们将在通过一个表单元中显示这两组信息。

8.5.2 向表视图单元添加子视图
默认的表视图只显示“一”行文本,即使你试图通过指定一个包含回车符的字符串让单元显示多个行,他也会删除回车符,并在下

一个单元的行中显示数据。现在我们要摆脱这种束缚。
使用基于视图的应用程序模版创建一个新Xcode项目,命名为Cells。打开CellsViewController.xib,添加一个TableView,然后像

我们在上一章做的,把委托和数据源设置为File's Owner。保存并返回Xcode。如果需要,请参考8.6.1的内容。

1,修改控制器头文件
#import <UIKit/UIKit.h>
#define kNameValueTag 1
#define kColorValueTag 2

@interface CellsViewController:UIViewController <UITableViewDataSource,UITableViewDelegate>
{ NSArray *computers;
}
@property (nonatomic,retain) NSArray *computers;
@end
我们会用自定义的两个常量为将要添加到表视图单元中的子视图分配标记(tag)。下面将向一个单元中添加4个子视图,其中2个需

要在每一行进行更改。为此,需要在使用特定的行数据更新单元时,通过某种机制检索单元中的两个字段。如果为需要再次使用的

每个标签设置唯一的标记值,那么将能够从表视图单元检索他们并设置他们的值。

2,实现控制器代码:
#import "CellsViewController.h"
@implementation CellsViewController
@synthesize computers;
- (void)viewDidLoad {
 NSDictionary *row1=[[NSDictionary alloc] initWithObjectsAndKeys:@"MacBook",@"Name",@"White",@"Color",nil];
 NSDictionary *row2=[[NSDictionary alloc] initWithObjectsAndKeys:@"MackBook Pro",@"Name",@"Silver",@"Color",nil];
 NSDictionary *rows=[[NSDictionary alloc] initWithObjectsAndKeys:@"iMac",@"Name",@"White",@"Color",nil];

 NSArray *array=[[NSArray alloc] initWithObjects:row1,row2,row3,nil];
 self.compters=array;
 
 [row1 release];
 [row2 release];
 [row3 release];
 [array release];
}

#pragma mark -
#pragma mark Table Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{return [self.computers count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *CellTableIdentifier=@"CellTableIdentifier";
  UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:CellTableIdentifier];
  if (cell==nil) {
   CGRect cellFrame=CGRectMake(0,0,300,65);
   cell=[[[UITableViewCell alloc] initWithFrame:cellFrame reuseIndentifier:CellTableViewIdentifier] autorelease];

   CGRect nameLableRect=CGRectMake(0,5,70,15);
   UILabel *nameLabel=[[UILabel alloc] initWithFrame:nameLabelRect];
   nameLabel.text=@"Name:";
   nameLabel.font=[UIFont boldSystemFontOfSize:12];
   [cell.contentView addSubView:nameLabel];
   [nameLabel release];

   CGRect colorLabelRect=CGRectMake(0,26,70,15);
   UILabel *colorLabel=[[UILabel alloc] initWithFrame:colorLabelRect];
   colorLabel.textAlignment=UITextAlignmentRight;
   colorLabel.text=@"Color:";
   colorLabel.font=[UIFont boldSystemOfSize:12];
   [cell.contentView addSubView:colorLabel];
   [colorLabel release];

   CGRect nameValueRect=CGRectMake(80,5,200,15);
   UILabel *nameValue=[[UILabel alloc] initWithFrame:nameValueRect];
   nameValue.tag=kNameValueTag;
   [cell.contentView addSubView:nameValue];
   [nameValue release];

   CGRect colorValueRect=CGRectMake(80,25,200,15);
   UILabel *colorValue=[[UILabel alloc] initWithFrame:colorValueFrame];
   colorValue.tag=kColorValueTag;
   [cell.contentView addSubView:colorValue];
   [colorValue release];
 }
 
 NSUInteger row=[indexPath row];
 NSDictionary *rowData=[self.computer objectAtIndex:row];
 UILabel *name=(UILabel *)[cell.contentView viewWithTag:kNameValueTag];
 name.text=[rowData objectForKey:@"Name"];

 UILabel *color=(UILabel *)[cell.contentView viewWithTag:kColorValueTag];
 color.text=[rowData objectForKey:@"Color"];
 
 return cell;
}
@end

向表视图添加视图比单独使用标准的表视图单元具有更大的灵活性。不过,通过编程创建、定位和添加所有的子视图是一项单调乏

味的工作。如果我们能在IB中设计表视图单元就好了,不是吗?

8.5.3 使用UITableViewCell的自定义子类
要想使用IB设计表视图单元,可以创建一个UITableViewCell子类和一个包含表视图单元的新nib文件。然后,当我们需要一个表视

图单元来表示一行时,不是向标准的表视图单元添加子视图,而是从nib文件加载子类,并使用将添加的两个输出口来设置名称和颜

色。
在Xcode中右键单击Classes文件夹,选择Add=》New File...,从新建文件向导中的左侧窗格选择Cocoa Touch Classes,然后从右

上窗格选择UITableViewCell子类。单击Next,命名为CustomCell.m,并确保选中了Also Create “CustomCell.h”复选框。
创建文件之后,在Resources文件夹上右键,选择Add=》New File...。这一次,在新建文件向导的左侧窗格中单击User Interfaces

,在右上窗格中选择Empty XIB。命名为CustomCell.xib。

1,创建UITableViewCell子类
单击CustomCell.h,添加以下代码:

IBOutlet UILabel *nameLabel;
IBOutlet UILabel *colorLabel;

2,在IB中设计表视图单元
打开CustomCell.xib,在库中找到表视图单元Table View Cell,然后将其拖动到主窗口中。
确保选中了表视图单元,然后Cmd+4,打开标识检查器。将类从UITableViewCell改为CustomCell。
然后Cmd+3,打开大小检查器,将表视图单元的高度从44改为65,这会让我们有更多的活动空间。
最后,按Cmd+1,打开属性检查器。其中第一个字段是Identifier,她是我们在代码中使用过的可重用的标识符。将其设置为

CustomCellIdentifier。下一步,找到名称为Accessory的弹出按钮,把Detail Discloure改为None。扩展图标会在单元中占用一些

空间,但是我们希望整个单元空间都归自己所用。
记住,即使UITableViewCell是UIView的子类,他仍然使用内容视图对子视图进行保存和分组。双击Custom Cell图标,将打开一个

新的窗口,你会发现一个标记为Content View的灰色虚线圆角矩形。
IB通过这种方式告诉你应该添加一些内容,因此在库中找到View并把它拖到Custom Cell窗口中。
发布视图之后会发现他的大小和窗口大小不一致,还要进行调整。选中新的视图,打开大小检查器。通过将x设置为0,y设置为0,w

设置为320,h设置为65,把View的大小和位置改为符合Custom Cell窗口的大小。
现在所有项都设置完成了,我们有了一个画布,可以使用他在IB中设计表视图单元。
拖4个标签,摆放位置,并重命名,设置字体。
现在,按下Ctrl键,并将Custom Cell图标拖到视图右上位置的标签,为他指定nameLabel输出口,和colorLabel输出口。
你可能会奇怪我们为什么米有做任何和File's Owner图标有关的事情。原因是根本不需要。我们使用这个表单元显示数据,不过与

用户的所有交互都是通过表视图来完成的,因此他不需要自己的控制器类。我们实际上指示使用nib作为一种模版,以便可视地设计

表单元。

3,使用新的表视图单元
要使用我们设计的单元,必须对CellsViewController.m中的tableView:cellForRowAtIndexPath方法做一些大的改动。删除当前的

tableView:cellForRowAtIndexPath方法,用下面的新版本代替:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)idnexPath
{ static NSString *CustomCellIdentifier=@"CustomCellIdentifier";
  CustomCell *cell=(CustomCell *)[tableView dequequeReusableCellWithIdentifier:CustomCellIdentifier];
  if (cell==nil) {
   NSArray *nib=[[NSBundle mainBundle] loadNibNamed:@"CustomCell" owner:self options:nil];
   cell=[nib objectAtIndex:1];
  }
  NSUInteger row=[indexPath row];
  NSDictionary *rowData=[self.compters objectAtIndex:row];
  cell.colorLabel.text=[rowData objectForKey:@"Color"];
  cell.nameLabel.text=[rowData objectForKey:@"Name"];
  return cell;
}

另外,别忘了#import "CustomCell.h"
我们使用objectAtIndex:调用中使用索引值1而不是0,因为对象0是文件的所有者,他并不是我们想要的。First Responder不是由

loadNibNamed:owner:options:返回的,因此表视图单元的索引值为1.
下面还要添加另一项内容。由于更改了表视图单元默认的高度值,我们必须告诉表视图这一事实;否则,表视图单元不会留下足够

的显示空间。谓词,在CellsViewController.m中添加以下委托方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{ return kTableViewRowHeight;
}
然后,我们不能从单元获得这个值,因为此委托方法可能在单元存在之前被调用,因此我们必须硬编码这个值。把这个常量定义在

CustomCell.h的顶部,然删除那些不再需要的tag常量。
#define kTableViewRowHeight 66
现在可以编译并运行程序了。

8.6 分组分区和索引分区
下一个项目将探讨表的另一个基本内容。仍然使用一个表视图(没有分层),不过我们将把数据分为几个分区。再次使用基于视图

的应用程序模版创建一个新的Xcode项目,这一次将它命名为Sections。

8.6.1 构建视图
打开Classes和Resources文件夹,在IB中双击SectionsViewController.xib打开文件。与之前一样,把表视图拖到View窗口中。然

后按下Cmd+2,将数据源和委托连接到File's Owner图标。
下一步,确保选中表视图,按下Cmd+1,打开属性检查器。把表视图的Style从Indexed改为Grouped。

8.6.2 导入数据
要完成此项目需要大量的数据,我们提供了另一个属性列表。08 Sections文件夹中找到sortednames.plist文件,把他添加到项目

的Resources文件中。
完成添加以后,单击sortednames.plist,看一下他到底是什么。她是包含字典的一个属性列表,其中字母表的每个字母都有一个条

目。每个字母下面是以该字母开头的名称列表。
我们将使用这个属性列表中的数据填充表视图,并为每个字母创建一个分区。

8.6.3 实现控制器
单击SectionsViewController.h文件,添加NSDictionary和NSArray实例变量及其相应的属性声明。字典将保存所有数据。数组将保

存以字母顺序排序的分区。别忘了遵循<UITableViewDataSource,UITableViewDelegate>
NSDictionary *names;
NSArray *keys;

现在,切换到SectionsViewController.m,

#pragma mark -
#prgma mark UIViewController Methods
- (void)viewDidLoad {
 NSString *path=[[NSBundle mainBundle] pathForResource:@"sortednames" ofType:@"plist"];
 NSDictionary *dict=[[NSDictionary alloc] initWithContentsOfFile:path];
 self.name=dict;
 [dict release];
 
 NSArray *array=[[names allKeys] sortedArrayUsingSelector:@selector(compare:)];
 self.keys=array;
}

#pragma mark -
#prgma mark Table View Data Source Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
 return [keys count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
 NSString *key=[keys objectAtIndex:section];
 NSArray *nameSection=[names objectForKey:key];
 return [nameSection count];
}
- (UITableView *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ NSUInteger section=[indexPath section];
 NSUInteger row=[indexPath row];

 NSString *key=[keys objectAtIndex:section];
 NSArray *nameSection=[names objectForKey:key];

 static NSString *SectionsTableIdentifier=@"SectionsTableIdentifier";
 UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:SectionsTableIdentifier];
 if (cell==nil) {
  cell=[[[UITableViewCell alloc]initWithFrame:CGRectZero resueIndentifier:SectionsTableIdentifier] autorelease];
 }
 cell.text=[nameSection objectAtIndex:row];
 return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{ NSString *key=[keys objectAtIndex:section];
  return key;
}

解析:
由于现在有了多个分区,所以多了几个数据源方法:
numberOfSectionsInTableView: 返回分区数量
tableView:numberOfRowsInSection:返回每个分区内的项的数量
tableView:titleForHeaderInSection:返回分区的标题

由于在SectionViewController.m中引用了CGRectZero,所以需要链接Core Graphics框架。
记住,我们已经把表的样式改为Grouped了。
做为比较,我们把表视图再次改为索引风格,然后看一下带有多个分区的索引试图是什么样子。

8.6.4 添加索引
当前表的一个问题是行数太多了。要找到特定的值,要翻好多页。
这个问题的一个解决方案是,在表视图的右侧添加一个索引。既然我们已经把表视图样式改成了索引类型,要添加一个索引相对来

说也算容易。在SectionsViewController.m文件尾部前增加一个方法:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{ return keys;
}

这就完成了。在这个方法中,委托请求一个值数组在索引中显示。必须使表视图中的多个分区使用索引,而且此数组中的条目必须

对应于这些分区。返回的数组拥有的条目数必须与拥有的分区数相同,且值必须对应于适当的分区。也就是说,此数组中的第一项

带给用户的是第一个分区,即分区0.
编译运行看效果!你将得到一个很好的索引。

8.7 实现搜索栏
我们将实现一个标准的iPhone搜索栏
8.7.1 重新考虑设计
在我们开始着手做之前,需要考虑一下应用程序是如何工作的。当前,我们拥有一个包含多个数组的字典,其中字母表中的每个字

母都占有一个数组。该字典是不可改变的,这就意味着不能从字典添加或删除值,它包含的数组也是如此。当用户取消搜索或者更

改搜索项时,还必须能够返回到源数据集。
我们能做的就是创建两个新的字典:一个包含完整数据集的不可改变的字典、一个可以从中删除行的可变的字典副本。委托和数据

源将从可变字典进行读取,当搜索标准更改或者取消搜索时,可以从不可改变的字典刷新可变字典。

8.7.2 深层可变副本
现在存在一个问题,NSDictionary遵循NSMutableCopying协议,该协议返回一个NSMutableDictionary,但是这个方法创建的是浅副

本。也就是说,调用mutableCopy方法时,他将创建一个新的NSMutableDictionary对象,该对象拥有源字典所拥有的对象。他们并

不是副本,而是相同的实际对象。如果我们所处理的字典仅存储字符串,这会很好,因为从副本中删除一个值不会对源字典产生任

何影响。然而,由于字典中存有数组,如果我们从副本的数组中删除对象,这些对象也将从源字典的数组中删除,因为副本和源都

指向相同的对象。
要解决这一问题,需要为存有数组的字典创建一个深层可变副本。并不难,不过我们应该把这项功能放在哪儿呢?
如果你说“在列别中”,那么好极了,你的想法是正确的。通过类别向现有对象添加附加方法,而不需要子类化这些对象。
通过类别,我们可以向NSDictionary添加一个方法来实现深层副本,返回的NSMutableDictionary拥有相同的数据,但不包含相同的

实际对象。
在项目窗口中,选择Classes文件夹,然后Cmd+N创建一个新文件。向导出现后,从最底端的左侧选择Other。然而,类别没有文件模

版,我们要创建一个空文件来保存它。选择Empty File图标,将第一个文件命名为NSDictionary-MutableDeepCopy.h。第二个文件

命名为NSDictionary-MutableDeepCopy.m。

提示:创建类别所需的两个文件的一个更快的方法就是,选择NSObject子类模版,然后删除文件内容。此选项同时提供头文件和实

现文件。

NSDictionary-MutableDeepCopy.h:

#import <Foundation/Foundation.h>
@interface NSDictionary(MutableDeepCopy)
- (NSMutableDictionary *)mutableDeepCopy;
@end

NSDictionary-MutableDeepCopy.m:
#importy "NSDictionary-MutableDeepCopy.h"

@implementation NSDictionary(MutableDeepCopy)
- (NSMutableDictionary *)mutableDeepCopy
{
  NSMutableDictionary *ret=[NSMutableDictionary dictionaryWithCapacity:[self count]];
  NSArray *keys=[self allKeys];
  for (id key in keys)
  { id oneValue=[self valueForKey:key];
    id oneCopy=nil;
  
    if ([oneValue respondsToSelector:@selector(mutableDeepCopy)])
     oneCopy=[oneValue mutableDeepCopy];
    else if ([oneValue respondsToSelector:@selector(mutableCopy)])
     oneCopy=[oneValue mutableCopy];
    if (oneCopy==nil)
     oneCopy=[oneValue copy];
    [ret setValue:oneCopy forKey:key];
  }
  return ret;
}
@end


8.7.3 更新控制器头文件
下一步,我们需要向控制器头文件添加一些输出口。表视图需要一个输出口。
我们还需要一个指向搜索栏的输出口,她是一个用于搜索的控件。
还需要一个附加字典
在控制器中不再需要任何新的操作方法,不过我们需要几个新的方法。目前,仅对他们进行声明。
还需要让类遵循UISearchBarDelegate协议。除了充当表视图的委托之外,我们还需要让他充当搜索栏的委托。

在SectionsViewController.h文件中做如下更改:

...
@interface SectionsViewController:UIViewController <UITableViewDataSource,UITableViewDelegate,UISearchBarDelegate>
{
 IBOutlet UITableView *table;
 IBOutlet UISearchBar *search;
 NSDictionary *allNames;
 NSMutableDictionary *names;
 NSMutableArray *keys;
}
...
- (void)resetSearch;
- (void)handleSearchForTerm:(NSString *)searchTerm;
@end


8.7.4 修改视图
在IB中打开SectionsViewController.xib。选中表视图,调整其大小,为顶部的搜索栏腾出一些空间。
从库中拖出一个Search Bar,将其添加到视图的顶部。
设置表视图和SearchBar的输出口。
选中SearchBar,按Cmd+1打开属性检查器。选中Show Cancel Button。设置占位符文本等
按下Cmd+2,转到连接检查器,设置Delegate为File's Owner。

8.7.5 修改控制器实现
对搜索栏进行的更改非常大。对SectionsViewController.m作如下更改,然后快速返回:

...
#import "NSDictionary-MutableDeepCopy.h"

...
#pragma mark -
#pragma mark Custom Methods
- (void)resetSearch
{ self.names=[self.allNames mutableDeepCopy];
  NSMutableArray *keyArray=[[NSMutableArray alloc] init];
  [keyArray addObjectsFromArray:[[self.allNames allKeys]
     sortedArrayUsingSelector:@selector(compare:)]];
  self.keys=keyArray;
  [keyArray release];
}

- (void)handleSearchFromTerm:(NSString *)searchTerm
{ NSMutableArray *sectionsToRemove=[[NSMutableArray alloc] init];
  [self resetSearch];
  
  for (NSString *key in self.keys) {
   NSMutableArray *array=[names valueForKey:key];
   NSMutableArray *toRemove=[[NSMutableArray alloc] init];
   for (NSString *name in array) {
    if ([name rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location==NSNotFound)
      [toRemove addObject:name];
   }
   if ([array count]==[toRemove count])
    [sectionsToRemove addObject:key];
   [array removeObjectsInArray:toRemove];
   [toRemove release];
  }
  [self.keys removeObjectsInArray:sectionsToRemove];
  [sectionsToRemove release];
  [table reloadData];
}

#pragma mark -
#pragma mark UIViewController Methods
- (void)viewDidLoad {
 NSString *path=[[NSBundle mainBundle] pathForResource:@"sortednames" ofType:@"plist"];
 NSDictionary *dict=[[NSDictionary alloc] initWithContentsOfFile:path];
 self.allNames=dict;
 [dict release];

 [self resetSearch];
 search.autocapitalizationType=UITextAutocapitalizationTypeNone;
 search.autocorrectionType=UITextAutocorrectionTypeNo;
}

#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{ return ([keys count]>0)?[keys count]:1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{ if ([keys count]==0)
   return 0;
 NSString *key=[key objectAtIndex:section];
 NSArray *nameSection=[names objectForKey:key];
 return [nameSection count];
}

- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{ NSUInteger section=[indexPath section];
 NSUInteger row=[indexPath row];

 NSString *key=[keys objectAtIndex:section];
 NSArray *nameSection=[names objectForKey:key];

 static NSString *sectionsTableIdentifier=@"sectionsTableIdentifier";
 UITableViewCell *cell=[aTableView dequeueReusableCellWithIdentifier:sectionsTableIdentifier];
 if (cell==nil) {
   cell=[[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:sectionsTableIdentifier] autorelease];
 }
 cell.text=[nameSection objectAtIndex:row];
 return cell;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{ if ([keys count]==0)
   return @"";
 NSString *key=[keys objectAtIndex:section];
 return key;
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{ return keys;
}

#pragma mark -
#pragma mark Table View Delegate Methods
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{ [search resignFirstResponder];
  return indexPath;
}

#pragma mark -
#pragma mark Search Bar Delegate Methods
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{ NSString *searchTerm=[searchBar text];
  [self handleSearchForTerm:searchTerm];
}

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchTerm
{ if ([searchTerm length]==0)
 {[self resetSearch];
  [table reloadData];
   return;
  }
 [self handleSearchTerm:searchTerm];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{ search.text=@"";
  [self resetSearch];
  [table reloadData];
  [searchBar resignFirstResponder];
}
@end

1,从allNames复制数据
2,实现搜索
3,修改viewDidLoad
4,修改数据源方法
5,添加表视图委托方法
6,添加搜索栏委托方法

8.8 小结
感觉怎么样?(好乱)这一章的内容极其重要,你已经学习了很多!你应该对平面表(flat table)的工作方式有了非常好的理解,并了解了如何自定义表和表视图单元,以及如何配置表视图。你还了解了如何实现搜索栏,在任何呈现大量数据的iPhone应用程序中,这都是一个至关重要的工具。
要确保真正理解了本章中的所有内容,因为本章是后面章节的基础。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值