MENU

UITableView

March 27, 2016 • Read: 2126 • Codes

UITableView 是 iOS 上最常用的组件了,几乎每个应用中都会用到。像 iOS 的「设置」应用,就几乎全部由 UITableView 组成。

UITableView 的组成

UITableView 的组成

一个 TableView 里有一个或多个 Section,Section 里包含了很多个 Cell(UITableViewCell),图上 Section 里每行都是一个 Cell,除此之外,每个 Section 还包含了一个 Header View 和一个 Footer View。

UITableView 的基本用法

在 Storyboard 中,我们可以直接将一个 Table View Controller 拖动到视图中来使用,这是系统帮我们实现好的一个 UITableViewController,不过这里,我们还是用另一种比较麻烦的方式来实现,这样可以帮助我们更好地理解 UITableView 的结构和使用方法。

新建一个基于 Storyboard 的 iOS 应用,然后在 Storyboard 中,我们在 Object library 中找到 Table View,然后拖到我们的视图中,这个时候运行应用是没有什么问题的,只是界面上会是一个空白的 TableView,因为 UITableView 要显示数据,要为其提供数据源(Data Source),要响应事件,我们还要为其提供 Delegate。

在 Storyboard 中为 UITableView 添加数据源和 delegate 很简单,只需在其 Connections Inspector 中将 dataSourcedelegate 分别拖向实现了 UITableViewDataSourceUITableViewDelegate 的类中即可。

在这里,我们选择模仿 UITableViewController 的实现方法:

  • 将 dataSource 和 delegate 链接到其 View Controller 中:

  • 然后在 View Controller 中实现 UITableViewDataSourceUITableViewDelegate 协议:

    @interface ViewController : UIViewController<UITableViewDataSource, UITableViewDelegate>
    
    @end
  • 在 ViewController 中实现必要的和可选的方法。

UITableViewDataSource 中有两个 @required 属性的方法是必须要实现的:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;

// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

UITableViewDelegate 里的方法均为 @optional

UITableViewDataSource

  • - numberOfSectionsInTableView:

    // 返回TableView里一共有几个Section,不重写的话默认返回1
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
        return 2;
    }
  • - tableView:numberOfRowsInSection:

    // Section中有几行Rows(UITableViewCell)
    // 参数section表示第几个Section,从0开始计数
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
        if (section == 0) {
            return 3;
        } else if (section == 1) {
            return 2;
        }
        return 1;
    }
  • tableView:cellForRowAtIndexPath:

    // 返回一个用于被展示的Cell
    // 参数 indexPath 里包含了 section 和 row 信息
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
        UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"];
        if (cell == nil) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyCell"];
        }
        if (indexPath.section == 0) {
            cell.imageView.image = [UIImage imageNamed:@"gear"];
        } else if (indexPath.section == 1) {
            cell.imageView.image = [UIImage imageNamed:@"guitar"];
        }
    
        cell.textLabel.text = [NSString stringWithFormat:@"%ld", indexPath.row];
        cell.detailTextLabel.text = [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row];
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        return cell;
    }
  • - tableView:viewForHeader/FooterInSection:

    //根据 Section 值返回 Section 的 Header 或 Footer 的值
    - (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
        UIView* header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"HeaderOrFooter"];
        if (!header) {
            header = [[UIView alloc] init];
        }
        if (section == 0) {
            header.backgroundColor = [UIColor lightGrayColor];
        } else if (section == 1) {
            header.backgroundColor = [UIColor darkGrayColor];
        }
        return header;
    }
    - (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
        UIView* footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"HeaderOrFooter"];
        if (!footer) {
            footer = [[UIView alloc] init];
        }
        if (section == 0) {
            footer.backgroundColor = [UIColor blueColor];
        } else if (section == 1) {
            footer.backgroundColor = [UIColor cyanColor];
        }
        return footer;
    }

UITableViewDelegate

  • - tableView:didSelectRowAtIndexPath:

    // 行被选中时执行的方法
    // 参数indexPath里包含了section 和 row信息
    - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%@", [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row]);
    }
  • - tableView:didDeselectRowAtIndexPath:

    // 行被取消选中时执行的方法,如选中新一行的时候,原来被选中的行就会执行此方法
    // 参数indexPath里包含了section 和 row信息
    - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"%@", [NSString stringWithFormat:@"Section: %ld - Row: %ld", indexPath.section, indexPath.row]);
    }

UITableViewCell 的使用

如果每需要一个 UITableViewCell 就新建一个,那么当一个 TableView 中行数很多时,就会对系统造成巨大的内存压力,所以我们一般在 tableView:cellForRowAtIndexPath: 等方法中,会先检查看有没有已经创建好的且当前没在使用的 Cell,有就直接拿来用,没有的话就新创建一个。

UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell Identifier"];
    if (cell == nil) {
          cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"Cell Identifier"];
    }

系统提供的 UITableViewCell

系统提供的 UITableViewCell

TableView Cell 定制

Cell 的高度

  • - tableView:heightForRowAtIndexPath:

这个方法会在每次 TableView 显示前挨个询问 TableViewCell 的高度,如果 TableView 中行数太多的话,开销会比较大。

  • tableView.rowHeight

如果我们确定 tableView 中每一行的高度都是一样的,那个可以在 tableView 中设置:

tableView.rowHeight = 100;

因为 - tableView:heightForRowAtIndexPath: 方法优先级比较高,所以在设置这个属性后就不应该再提供 - tableView:heightForRowAtIndexPath: 方法了,否则会没有效果。

rowHeight 属性默认值为 UITableViewAutomaticDimension,系统会自动计算 Cell 的高度,除非已经定义了 Cell的高度(如:Storyboard 中)。

  • - tableView:estimatedHeightForRowAtIndexPath:

// Use the estimatedHeight methods to quickly calcuate guessed values which will allow for fast load times of the table.
// If these methods are implemented, the above -tableView:heightForXXX calls will be deferred until views are ready to be displayed, so more expensive logic can be placed there.

提供一个估算的值,这样,- tableView:heightForRowAtIndexPath: 方法的调用会推迟到 Cell(不是 TableView)即将被显示时。

可视化定制 Cell - Prototype Cell

首先将 TableView 的 Content 属性设置为 Dynamic Prototypes,然后在 Object library 中找到 Table View Cell,然后将其拖至 TableView 中,然后定制其即可,这样就创建好了一个 Cell 模板。

使用时:

UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Cell Identifier"];

这样就可以获得一个 Cell 了,这种况下其不会返回 nil,如果没有会自动创建一个然后返回。

我们可以通过 - viewWithTag: 方法来获取 cell 内部的控件。

这么做会有些麻烦,因为获取过来后还是需要强制类型转换后才能使用。

我们可以在创建 Cell 模板的时候,将 Cell 的 Class 设置成自定义的 Class(集成自 UITableViewCell ),然后把控件链接到 IBOutlet 上,并暴露出来,这样我们就可以方便的访问其控件了:

CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:@"Custom Cell Identifier"];
// customLabel 为链接到空间上的 IBOutlet 属性
cell.customLabel.text = @"Whatever";

可视化定制 Cell - 从 Xib 中加载

New->File,选择 Cocoa Touch Class,然后创建一个 UITableViewCell 的子类,并勾选 Also create XIB file。

然后在 Xib 文件中定制 Cell,并关联至 Class 中。

使用:

  • 方法一:

    // 在合适的地方:如viewDidLoad等处
    UINib* nib = [UINib nibWithNamed:@"XibCellFileName" bundle:bundle];
    [self.tableView registerNib:nib forCellReuseIdentifier:@"Cell Identifier"];

其后的使用和之前一致。

  • 方法二:

    // 在合适的地方:如 viewDidLoad 等处
    [self.tableView registerClass:[XibCellClass class] forCellReuseIdentifier:@"Cell Identifier"];

注意:要使用此方法,需要在 XibCellClass 中实现 - initWithStyle: reuseIdentifier: 方法:

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    NSArray *uiObjects = [[BSBundle mainBundle] loadNibNamed:@"XibCellFileName" owner:self options:nil];
    self = uiObjects[0];// Find in uiObjects
    return self;
}

其后的使用和之前一致。

使用代码控制 UITableView

选中 Row

  • -[UITableView selectRowAtIndexPath:amimated:scrollPosition:]

    // 使用代码选中指定行
    - (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition;
    /*
    UITableViewScrollPosition 参数
    typedef NS_ENUM(NSInteger, UITableViewScrollPosition) {
        UITableViewScrollPositionNone,// 不自动滚动选中的行
        UITableViewScrollPositionTop,// 将选中的行滚动到 TableView 可见区域的顶部
        UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间
        UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部
    }; 
    */
  • -[UITableView deselectRowAtIndexPath:amimated:]

    // 使用代码取消选中指定行
    - (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated;

读取被选中的行

// 当前选中的行的 IndexPath(单选)
// returns nil or index path representing section and row of selection.
@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow; 

// 当前选中的行的 IndexPath(多选)
// returns nil or a set of index paths representing the sections and rows of the selection.
@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForSelectedRows NS_AVAILABLE_IOS(5_0); 

控制表格滚动

  • -[UITableView scrollToRowAtIndexPath:atScrollPosition:animated:]

    // 滚动指定行至 TableView 的可见区域
    - (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
    /*
    UITableViewScrollPosition 参数
    在这里 UITableViewScrollPosition 参数和之前选中行时的行为略有不同
    typedef NS_ENUM(NSInteger, UITableViewScrollPosition) {
        UITableViewScrollPositionNone,// 将选中的行就近滚动到 TableView 的可见区域
        UITableViewScrollPositionTop,   // 将选中的行滚动到 TableView 可见区域的顶部
        UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间
        UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部
    }; 
    */
  • -[UITableView scrollToNearestSelectedRowAtScrollPosition:scrollPosition:animated:]

    // 将最近被选中的行滚动至TableView的可见区域
    - (void)scrollToNearestSelectedRowAtScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
    /*
    UITableViewScrollPosition 参数
    在这里 UITableViewScrollPosition 参数和之前选中某一行时的行为略有不同
    typedef NS_ENUM(NSInteger, UITableViewScrollPosition) {
        UITableViewScrollPositionNone,// 将选中的行就近滚动到 TableView 的可见区域
        UITableViewScrollPositionTop,   // 将选中的行滚动到 TableView 可见区域的顶部
        UITableViewScrollPositionMiddle,// 将选中的行滚动到 TableView 可见区域的中间
        UITableViewScrollPositionBottom // 将选中的行滚动到 TableView 可见区域的底部
    }; 
    */

刷新 UITableView

当数据源发生变化时,TableView 并不能知道其已经发生了变化,所以需要我们手动对其刷新。

TableView 的刷新方法有以下几种:

// 刷新整个 TableView
// reloads everything from scratch. redisplays visible rows. because we only keep info about visible rows, this is cheap. will adjust offset if table shrinks
- (void)reloadData;
// 仅刷新指定行
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);  
// 仅刷新指定的一个或多个Section
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);
// 刷新索引Bar
- (void)reloadSectionIndexTitles NS_AVAILABLE_IOS(3_0);

UITableView的编辑模式

UITableView 内建了表格编辑支持,但其仅会对表格进行编辑,数据源需要我们自己更新。

开启表格编辑模式很简单,只需将其 editing 属性设置为 YES 即可开启,关闭时设为 NO 即可。

// 开启编辑模式
self.tableview.editing = YES;
// 关闭编辑模式
self.tableview.editing = NO;

编辑模式

开启编辑模式后,Row 左边就会出现如图所示的红色带减号圆点、绿色带加号圆点以及一些没有圆点的 Row,这些样式就是UITableViewCellEditingStyle,要实现以上效果,我们可以实现 UITableViewDelegate 中的如下几个方法:

// 为指定行提供编辑模式的样式(如上图每行左边的圆点),在编辑模式开启后,TableView 会对其下的所有支持编辑的Row逐一询问。
// 如不重写此方法,则默认返回 UITableViewCellEditingStyleDelete
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;
/*
返回值
typedef NS_ENUM(NSInteger, UITableViewCellEditingStyle) {
    UITableViewCellEditingStyleNone,    // 没有标记,显示为空白
    UITableViewCellEditingStyleDelete,    // 显示红色带减号圆点
    UITableViewCellEditingStyleInsert    // 显示绿色带加号圆点
};
*/
//指定行是否支持编辑,在编辑模式开启后,TableView 会对其下的 Row 逐一询问,如果返回 YES,则会访问- tableView:editingStyleForRowAtIndexPath: 询问编辑模式样式
//如不重写此方法,则默认返回 YES
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;
// 响应删除、新建操作,我们可以在此方法中删除、添加 Row,并更新数据源
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;
// 指定行是否支持移动操作,在编辑模式开启后,TableView 会对其下的 Row 逐一询问
// 如不重写此方法,则默认返回 YES
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;
// 提供该方法,编辑模式就会显示移动控件(如编辑模式图中作图所示),在这里可以响应移动操作
// 只有 - tableView:canMoveRowAtIndexPath: 方法返回 YES 的行会显示
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;

添加、删除 Row 及 Section

// 添加、删除 Section
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
// 添加、删除 Row
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

另外,当我们一次添加/删除多个 Row/Section 时,可以将其放入:

[self.tableview beginUpdates];
//这里
[self.tableview endUpdates];

这样,可以通过一次动画完成多次操作。

带索引的表格

// return list of section titles to display in section index view (e.g. "ABCD...Z#")
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView __TVOS_PROHIBITED;                                                    
// fixed font style. use custom view (UILabel) if you want something different
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;
// tell table which section corresponds to section title/index (e.g. "B",1))
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index __TVOS_PROHIBITED; 

索引的本地化

http://nshipster.cn/uilocalizedindexedcollation/UITableView

Tags: iOS, iOS开发
Archives QR Code Tip
QR Code for this page
Tipping QR Code
Leave a Comment

已有 1 条评论
  1. 你为何辣么屌