iOS TableView 编程指导(四)-详细地看看table
tableView使用cell对象(UITableViewCell
的实例)绘制可见的row, 然后缓存这些cell. cell有DataSource通过方法tableView:cellForRowAtIndexPath:
提供.
在本文中, 将会了解以下知识点:
- cell的特性
- 如何使用
UITableViewCell
自带能力来设置cell的内容 - 如何创建一个自定义的
UITableViewCell
对象
cell的特性
一个cell包含多个不同的部分, 每一个部分可能随着tableView的模式(编辑模式/正常模式)改变而改变. 正常情况下, cell包含:text, image, 或者其他区别标识符三部分. 如图4-1展示cell的几个主要部分
图4-1 tableView中cell的组成
右边的小部分区域时用来存放AccessoryView的比如:disclosure indicator, detail disclosure control, 和其他常见的control(switch, slider等), 另外还可以是custom view.
但是当tableView处于编辑模式时, 编辑控件会显示在cell(如果该cell已经设置好)的最左边的区域, 如图4-2所示.
图4-2 处于编辑模式下cell的内容组成
这个编辑控件可以是一个删除按钮(一个红色减号加上一个圈)也可以是一个插入按钮(一个绿色的加号加上一个圈). 整个cell中的内容向右挪动以空出地方显示编辑控件. 如果cell设置可以自由排序, 那么会有一个排序控件出现在cell的右侧(在accessory view的右侧). 排序控件是几根短横线, 按住排序控件拖动cell就可以对cell进行自由排序了.
如果一个cell是复用的, 你在storyboard给该cell设置一个任意字符串作为复用标识符(一个典型的列子). 在运行期, tableView会将cell保存在一个内部队列(internal queue)中. 当tableView让DataSource去设置一个将要显示的cell时, DataSource对象通过调用tableView的dequeueReusableCellWithIdentifier:
方法来访问内部队列中cell, 只需传一个标识符就行了. 然后DataSource拿到该cell后就会对其内容和其他属性的一些设置, 之后才是将该cell返回给tableView. 这样做的原因当然处于性能的考虑, 避免了重复创建大量的cell.
TableView中的row可以同时出现多种样式的cell, 可以是系统预定义的四种, 或者是用户自定义的cell, 这样在cell复用时, 需要用一个identifier来表示每种cell, 因此TableView中的内部队列中, 保存多种使用标识符标记的cell.
有三种方式为TableView提供cell:
- 使用系统预定的几种style创建
- 在storyboard中定制cell, 往cell中添加subview, 再从storyboard加载cell
- 继承
UITableViewCell
, 往cell的的contentView中添加内容.
注意, cell的contentView是一个容器, 本身并不显示.
使用cell的预定义样式
可以使用UITableViewCell
来直接创建预定义风格的cell, 系统定义了四种风格cell, 你可以使用现成的风格来创建cell, 本系列文章第一篇已详细讲解过, 下面代码展示着四种风格代码表示.
typedef enum {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
} UITableViewCellStyle;
这四种cell内部包括两部分:一个/多个text,一个image(可隐藏). 如下图
图4-3 cell的系统默认风格
类UITableViewCell
定义了三个属性来表示它的内容:
-
textLabel
-一个label用来展示title -
detailTextLabel
-一个label用来展示subtitle(展示详细内容) -
imageView
-一个image view用来显示image
图4-4展示了直接使用UITableViewCell
创建cell的例子(该例中cell的风格是UITableViewCellStyleSubtitle
).
代码4-1, 展示tableView:cellForRowAtIndexPath:
中实现图4-4中的cell.
代码清单4-1 配置UITableViewCell
中的text和image
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
NSDictionary *item = (NSDictionary *)[self.content objectAtIndex:indexPath.row];
cell.textLabel.text = [item objectForKey:@"mainTitleKey"];
cell.detailTextLabel.text = [item objectForKey:@"secondaryTitleKey"];
NSString *path = [[NSBundle mainBundle] pathForResource:[item objectForKey:@"imageKey"] ofType:@"png"];
UIImage *theImage = [UIImage imageWithContentsOfFile:path];
cell.imageView.image = theImage;
return cell;
}
在上面方法中, 每次复用cell时, 都应该刷新cell的内容.
在设置cell时, 你还可以:
-
selectionStyle
- 控制cell选中时的外观 -
accessoryType
和accessoryView
- 让你可以设置标准(非编辑状态)的accessoryView(disclosure indicator or detail disclosure control)或者自定义的accessoryView, 自定义时, 你需要提供UIView对象, 比如switch, slider等. -
editingAccessoryType
和editingAccessoryView
- 让你可以设置标准(编辑状态)的accessoryView(disclosure indicator or detail disclosure control)或者自定义的accessoryView, 自定义时, 你需要提供UIView对象, 比如switch, slider等. -
showsReorderControl
-控制cell是否显示排序控件(编辑模式下). 有个与之相关的属性editingStyle
, 该属性代表cell中编辑控件的风格, 是只读的, 需要通过来delegate方法tableView:editingStyleForRowAtIndexPath:
来控制. -
backgroundView
和selectedBackgroundView
- 为cell提供一个背景(分选中和非选中) -
indentationLevel
和indentationWidth
- 设置cell中内容的缩进等级和缩进等级的width
因为cell继承自UIView
, 所以你也可以对cell设置UIView
中的属性, 比如设置背景颜色. 代码4-2展示了在delegate方法tableView:willDisplayCell:forRowAtIndexPath:
来修改行的背景颜色
代码清单4-2 修改cell的背景颜色
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row%2 == 0) {
UIColor *altCellColor = [UIColor colorWithWhite:0.7 alpha:0.1];
cell.backgroundColor = altCellColor;
}
}
代码4-2说明了TableView API设计的一个重要特性. 在TableView将要绘制一行时, 会发送一个tableView:willDisplayCell:forRowAtIndexPath:
给到delegate, 这样可以在绘制前提供一个做最后的修改
的机会给你. 在该方法中你应该对cell只做一些基于状态的修改, 而不是修改cell的content.
自定义cell
使用系统预定义的四种样式的cell, 虽然可以满足很多场景的需求, 但是如果你想更改cell中的内容组成(不单单是text, image, 或者数量上的变化), 内容的layout变化, 或者cell的一些特性, 那么此时预定义的样式没法满足需求. 下面有两个解决方案:
- 往cell的contentView中添加subview
- 创建自定义cell,新建一个
UITableViewCell
的子类
接下来的内容是对上面两个方案的讲解.
从storyboard中加载tableView的cell
在storyboard中, TableView有两种cell, dynamic和static. 如果TableView需要显示大量的cell, 那么选中dynamic; 如果在编译期就知道cell的数量, 那么选择static; 选中TableView中的cell, 下一级展示该item的详细信息时, static是不错的选择.
你可以通过TableView对象直接设置cell的static或者dynamic样式. 图4-5中, 展示master-detail类型的APP中TableView设计例子, 在该列中, master tableView用的dynamic, detail TableView用的是static.
图4-5 storyboard中的TableView cell
设置动态(dynamic)cell
在本小节中, 你使用custom的cell原型创建TableView, 在运行期, DataSource对象dequeue cell, 设置cell, 然后丢给TableView去绘制, 效果如图4-6
图4-6 使用custom prototype的cell
DataSource可以用两种不同的方式去访问cell中的subview. 第一种是使用UIView的tag
属性, 另外一种是使用storyboard中的outlet. 使用tag
是较方便的, 但是这样会使得代码中的tag和storyboard中的tag数字耦合在一起, 使得代码缺乏灵活性. 使用第二方法比较麻烦点, 不仅需要设置storyboard还需要创建UITableViewCell
的子类, 下面就这两方法具体讲解.
创建使用故事板加载自定义表视图单元格的项目
- 使用“Master-Detail应用程序模板”创建项目并选择“使用故事板”选项
- 在故事板画布上,选择主视图控制器
- 在身份检查器(Identity inspector)中,验证该类是否被设置为自定义MasterViewController控制器类
- 选择主视图控制器内部的表视图
- 在属性检查器中,验证内容弹出菜单是否设置为动态原型(Dynamic Prototypes)
- 选择原型单元格
- 在属性检查器中,在样式弹出菜单中选择“自定义”
- 在标识符文本字段中输入重用标识符。
这是您在dequeueReusableCellWithIdentifier:
:消息中发送到表视图的相同的 reuse identifier. 例如,请参见清单4-3。 - 在Accessory弹出菜单中选择Disclosure Indicator
- 将对象从库拖动到单元格上。
对于这个例子,拖动两个标签对象并将它们放置在单元格的结尾附近(为附件视图留出空间)。 - 选择对象并设置它们的属性、大小和自动恢复特性。
此步骤一个关键点是为cell中的每个部分设置一个tag。在属性检查器的视图部分中找到此属性,并为每个对象分配唯一的整数。
现在, 你开始手动获取TableView的数据(对应本例而言, 只需要获取每个cell的行数), 代码4-3是tableView:cellForRowAtIndexPath:
的实现, 使用当前行号来填充tableView.
代码清单4-3 使用tag来为cell设置数据
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];
UILabel *label;
label = (UILabel *)[cell viewWithTag:1];
label.text = [NSString stringWithFormat:@"%d", indexPath.row];
label = (UILabel *)[cell viewWithTag:2];
label.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS - indexPath.row];
return cell;
}
注意:因为使用storyboard, 所以
dequeueReusableCellWithIdentifier:
总会返回一个有效的cell, 所以不会对cell做nil判断.
如果不喜欢使用tag,可以使用另一种方法来设置单元格中的内容。为cell定义一个UITableViewCell
子类, 然后定义一些自定义属性。在故事板中,将新类与原型单元格相关联,并将outlet连接到单元格中的相应对象。
为自定义单元格的内容使用outlet
-
将Objective-C类名为MyTableViewCell单元格添加到项目中。
-
将以下代码添加到MyTableViewCell.h:
@interface MyTableViewCell : UITableViewCell @property (nonatomic, weak) IBOutlet UILabel *firstLabel; @property (nonatomic, weak) IBOutlet UILabel *secondLabel; @end
-
将以下代码行添加到实现DataSource协议的源文件中:
#import "MyTableViewCell.h"
-
使用身份检查器将原型单元格的类设置为
MyTableViewCell
-
使用连接检查器将原型cell中的两个outlet连接到相应的标签
image -
按照代码4-4所示来实现
tableView:cellForRowAtIndexPath:
方法
代码清单4-4 使用outlet来为cell添加数据
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"];
cell.firstLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];
cell.secondLabel.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS - indexPath.row];
return cell;
}
设置静态(static)cell
本小节讲解使用静态cell来创建tableView, 效果如图4-7所示:
图4-7 静态cell
和前面dynamic步骤一样, 首先是往项目中添加UITableViewController
的子类. 为第一个cell中的master row label和最后一个cell中的slider, slider value label创建三个outlet, 如代码4-5所示
代码清单4-5 为static cell定义三个属性outlet
@interface DetailViewController : UITableViewController
@property (strong, nonatomic) id detailItem;
@property (weak, nonatomic) IBOutlet UILabel *masterRowLabel;
@property (weak, nonatomic) IBOutlet UILabel *sliderValueLabel;
@property (weak, nonatomic) IBOutlet UISlider *slider;
- (IBAction)logHello;
- (IBAction)sliderValueChanged:(UISlider *)slider;
@end
在故事板中,将表视图控制器对象从库拖动到画布上。选择表视图并在属性检查器中设置以下属性:
- 将内容弹出菜单设置为静态单元格。
- 将节数设置为2。
- 将样式弹出菜单设置为分组
对于表视图中的每个部分,使用属性检查器在标题字段中输入字符串。然后,对于cell,完成以下步骤:
- 删除第一个表视图部分中的三个单元格和第二节中的一个单元格
- 根据需要增加每个剩余单元的高度。
- 从库中拖动对象以组成每个单元的子视图,如图4-7所示
不必为这些单元格分配重用标识符,因为您不打算实现DataSource方法tableView:cellForRowAtIndexPath:
- 设置这些对象的任何你期望的属性
该列中的slider具有7.5的初始值, 范围是0-10.
选择表格视图控制器和显示器的连接检查。在表视图控制器和对应对象之间的三个outlet之间建立连接,如图4-8所示。在完成此操作时,实现清单4-5中声明的两个动作方法,并将目标动作连接至button和slider。
图4-8 给你static cell中的内容建立连接
为了给cell填充数据, 你需要实现detail controller中configureView
方法
代码清单4-6 设置数据
- (void)configureView {
if (self.detailItem) {
self.masterRowLabel.text = [self.detailItem description];
}
self.sliderValueLabel.text = [NSString stringWithFormat:@"%1.1f", self.slider.value];
}
detail view Controller 会在viewDidLoad
中调用configureView
和setDetailItem:
方法.
使用代码的方式往cell中contentView中添加subview
一个cell是用来展示tableView中的行的, cell是一个view(UITableViewCell
继承自UIView
). 作为一个view, 一个cell拥有一个contentView(cell中各种content的superview, 一个容器), 通过往content view中添加subview, 和对这些subview进行layout来自定义tableView中row的外观, 要访问cell的contentView可以通过cell的属性contentView
.
这种方式相对简单; 因为不需要创建UITableViewCell
的子类来实现自定义cell中的内容. 但是, 在使用这种方式时, 你尽可能地避免将view的设置为透明, 因为透明子视图会影响滚动性能, 因为这会增加合成视图的成本. subview应该是设置为不透明, 通常设置和cell有相同的BackgroundColor. 如果cell是可选的话, 当选中cell时注意要将cell的content设置为highlighted. 当cell中的subview实现了属性highlighted
的accessor method
(set/get方法)的话, 当cell选中时cell的content也会自动被选中的.
假设你cell中的text和Image的位置都需要自定义, 例如, 你想将Image置于cell的右边, title和subtitle右对齐并和Image紧靠. 如图4-9所示
图4-9 自定义cell中subview的位置
代码4-7描述了DataSource的代码实现方法tableView:cellForRowAtIndexPath:
. 首先是使用重用标识符来获取复用cell. 如果复用cell不存在, 就创建两个label(subtitle和title)和imageView, 设置content的frame, 然后将这些content放入新创建的cell中, 并用tag标记每一个content. 如果复用cell存在, 就直接使用viewWithTag
来获取上面创建的三个content, 最后对content赋值.
代码清单4-7 往cell中添加subview
#define MAINLABEL_TAG 1
#define SECONDLABEL_TAG 2
#define PHOTO_TAG 3
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"ImageOnRightCell";
UILabel *mainLabel, *secondLabel;
UIImageView *photo;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
mainLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 220.0, 15.0)];
mainLabel.tag = MAINLABEL_TAG;
mainLabel.font = [UIFont systemFontOfSize:14.0];
mainLabel.textAlignment = UITextAlignmentRight;
mainLabel.textColor = [UIColor blackColor];
mainLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
[cell.contentView addSubview:mainLabel];
secondLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 20.0, 220.0, 25.0)];
secondLabel.tag = SECONDLABEL_TAG;
secondLabel.font = [UIFont systemFontOfSize:12.0];
secondLabel.textAlignment = UITextAlignmentRight;
secondLabel.textColor = [UIColor darkGrayColor];
secondLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
[cell.contentView addSubview:secondLabel];
photo = [[UIImageView alloc] initWithFrame:CGRectMake(225.0, 0.0, 80.0, 45.0)];
photo.tag = PHOTO_TAG;
photo.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
[cell.contentView addSubview:photo];
} else {
mainLabel = (UILabel *)[cell.contentView viewWithTag:MAINLABEL_TAG];
secondLabel = (UILabel *)[cell.contentView viewWithTag:SECONDLABEL_TAG];
photo = (UIImageView *)[cell.contentView viewWithTag:PHOTO_TAG];
}
NSDictionary *aDict = [self.list objectAtIndex:indexPath.row];
mainLabel.text = [aDict objectForKey:@"mainTitleKey"];
secondLabel.text = [aDict objectForKey:@"secondaryTitleKey"];
NSString *imagePath = [[NSBundle mainBundle] pathForResource:[aDict objectForKey:@"imageKey"] ofType:@"png"];
UIImage *theImage = [UIImage imageWithContentsOfFile:imagePath];
photo.image = theImage;
return cell;
}
上面的代码中使用tag标记cell中的subview的好处时, 在delegate活DataSource中获取指定的cell后, 你可以随时通过tag来访问cell的subview, 然后对这些subview赋值呀, 重新设置等等操作, 这方很方便.
然后你看到了, 上面创建cell时使用的UITableViewCell
使用系统样式UITableViewCellStyleDefault
而不是自定义的cell, 这是因为系统预定义的cell中的textLabel
,detailTextLabel
和imageView
这些subview在你给它们设置内容之前是nil的, 所以你往系统的cell添加subview时, 不会和系统的subview造成冲突.
注意:实现上面的功能还有另外一种办法就是, 继承
UITableViewCell
, 并在实现文件中重写layoutSubViews
方法, 在该方法中重新布局textLabel
,detailTextLabel
和imageView
, 记得要先调用super方法.
使用文本内容实现“attributed string”效果的一种方法是布局UITabeleCell中contentView中UILabel子视图。每个Label的文本可以有自己的字体,颜色,大小,对齐方式和其他特征。如果您想在标签对象中使用这种变体,请创建多个标签并将它们相对于彼此放置
增强表视图单元的可访问性
该内容, 是和VoiceOver(残疾人用的到)有关的, 我这里不作讲解, 有兴趣可以自己去看Enhancing the Accessibility of Table View Cells
关于cell和tableView的性能 ※※※
关于如何使用tableView的cell, 比如使用系统的cell还是自定义, 有个关键因素是tableView的性能, 在使用tableView时, 应该关注下面几个点:
- 复用cell 创建一个对象是需要消耗的, 特别是在大量重复创建对象时, 比如用户滚动tableView浏览大量数据时. 所以如果你复用一个cell而不是重复创建, 这样对app的性能提升有很大的帮助.
- 避免重复布局content 当重复使用具有自定义子视图的cell时,每次tableView请求cell时不要布置这些子视图。创建单元格时,将子视图布置一次就好。
- 使用不透明的subview 创建cell的subview,尽量设置这些cell的opaque为YES, 这样对性能提高有帮助.