ios漫漫学习路

iOS TableView 编程指导(四)-详细地看看table

2018-10-20  本文已影响72人  陵无山

tableView使用cell对象(UITableViewCell的实例)绘制可见的row, 然后缓存这些cell. cell有DataSource通过方法tableView:cellForRowAtIndexPath:提供.

在本文中, 将会了解以下知识点:

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:

  1. 使用系统预定的几种style创建
  2. 在storyboard中定制cell, 往cell中添加subview, 再从storyboard加载cell
  3. 继承UITableViewCell, 往cell的的contentView中添加内容.
    注意, cell的contentView是一个容器, 本身并不显示.

使用cell的预定义样式


可以使用UITableViewCell来直接创建预定义风格的cell, 系统定义了四种风格cell, 你可以使用现成的风格来创建cell, 本系列文章第一篇已详细讲解过, 下面代码展示着四种风格代码表示.

typedef enum {
    UITableViewCellStyleDefault,
    UITableViewCellStyleValue1,
    UITableViewCellStyleValue2,
    UITableViewCellStyleSubtitle
} UITableViewCellStyle;

这四种cell内部包括两部分:一个/多个text,一个image(可隐藏). 如下图


图4-3 cell的系统默认风格

UITableViewCell定义了三个属性来表示它的内容:

图4-4展示了直接使用UITableViewCell创建cell的例子(该例中cell的风格是UITableViewCellStyleSubtitle).

图4-4 UITableViewCellStyleSubtitle风格的cell

代码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时, 你还可以:

因为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的一些特性, 那么此时预定义的样式没法满足需求. 下面有两个解决方案:

从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的子类, 下面就这两方法具体讲解.

创建使用故事板加载自定义表视图单元格的项目

  1. 使用“Master-Detail应用程序模板”创建项目并选择“使用故事板”选项
  2. 在故事板画布上,选择主视图控制器
  3. 在身份检查器(Identity inspector)中,验证该类是否被设置为自定义MasterViewController控制器类
  4. 选择主视图控制器内部的表视图
  5. 在属性检查器中,验证内容弹出菜单是否设置为动态原型(Dynamic Prototypes)
  6. 选择原型单元格
  7. 在属性检查器中,在样式弹出菜单中选择“自定义”
  8. 在标识符文本字段中输入重用标识符。
    这是您在dequeueReusableCellWithIdentifier::消息中发送到表视图的相同的 reuse identifier. 例如,请参见清单4-3。
  9. 在Accessory弹出菜单中选择Disclosure Indicator
  10. 将对象从库拖动到单元格上。
    对于这个例子,拖动两个标签对象并将它们放置在单元格的结尾附近(为附件视图留出空间)。
  11. 选择对象并设置它们的属性、大小和自动恢复特性。
    此步骤一个关键点是为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

  1. 将Objective-C类名为MyTableViewCell单元格添加到项目中。

  2. 将以下代码添加到MyTableViewCell.h:

    @interface MyTableViewCell : UITableViewCell
    
    @property (nonatomic, weak) IBOutlet UILabel *firstLabel;
    @property (nonatomic, weak) IBOutlet UILabel *secondLabel;
    @end
    
  3. 将以下代码行添加到实现DataSource协议的源文件中:

     #import "MyTableViewCell.h"
    
  4. 使用身份检查器将原型单元格的类设置为MyTableViewCell

  5. 使用连接检查器将原型cell中的两个outlet连接到相应的标签


    image
  6. 按照代码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

在故事板中,将表视图控制器对象从库拖动到画布上。选择表视图并在属性检查器中设置以下属性:

  1. 将内容弹出菜单设置为静态单元格。
  2. 将节数设置为2。
  3. 将样式弹出菜单设置为分组

对于表视图中的每个部分,使用属性检查器在标题字段中输入字符串。然后,对于cell,完成以下步骤:

  1. 删除第一个表视图部分中的三个单元格和第二节中的一个单元格
  2. 根据需要增加每个剩余单元的高度。
  3. 从库中拖动对象以组成每个单元的子视图,如图4-7所示
    不必为这些单元格分配重用标识符,因为您不打算实现DataSource方法tableView:cellForRowAtIndexPath:
  4. 设置这些对象的任何你期望的属性
    该列中的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中调用configureViewsetDetailItem:方法.

使用代码的方式往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实现了属性highlightedaccessor 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,detailTextLabelimageView这些subview在你给它们设置内容之前是nil的, 所以你往系统的cell添加subview时, 不会和系统的subview造成冲突.

注意:实现上面的功能还有另外一种办法就是, 继承UITableViewCell, 并在实现文件中重写layoutSubViews方法, 在该方法中重新布局textLabel,detailTextLabelimageView, 记得要先调用super方法.

使用文本内容实现“attributed string”效果的一种方法是布局UITabeleCell中contentView中UILabel子视图。每个Label的文本可以有自己的字体,颜色,大小,对齐方式和其他特征。如果您想在标签对象中使用这种变体,请创建多个标签并将它们相对于彼此放置

增强表视图单元的可访问性


该内容, 是和VoiceOver(残疾人用的到)有关的, 我这里不作讲解, 有兴趣可以自己去看Enhancing the Accessibility of Table View Cells

关于cell和tableView的性能 ※※※


关于如何使用tableView的cell, 比如使用系统的cell还是自定义, 有个关键因素是tableView的性能, 在使用tableView时, 应该关注下面几个点:

上一篇下一篇

猜你喜欢

热点阅读