iOS文档补完计划--UITableView

目录

  • UITableView
  • 复用原理
  • UITableViewDataSource&&UITableViewDelegate
  • 配置TableView
    • style
    • numberOfRowsInSection:
    • numberOfSections
    • rowHeight
    • separator
    • backgroundView
    • cellLayoutMarginsFollowReadableWidth
  • 创建Cell
    • registerCell
    • dequeueReusableCellWithIdentifier:forIndexPath:
    • dequeueReusableCellWithIdentifier:
  • 页眉和页脚
  • Cell&&IndexPath
    • cellForRowAtIndexPath:
    • indexPathForCell:
    • indexPathForRowAtPoint:
    • indexPathsForRowsInRect:
    • visibleCells
    • indexPathsForVisibleRows
  • 预估高度
  • 滚动tableView
    • scrollToRowAtIndexPath:atScrollPosition:animated:
    • scrollToNearestSelectedRowAtScrollPosition:animated:
  • 选择控制
    • indexPathForSelectedRow
    • indexPathsForSelectedRows
    • selectRowAtIndexPath:animated:scrollPosition:
    • deselectRowAtIndexPath:animated:
    • 选择权限
  • 插入&&删除&&移动
    • performBatchUpdates:completion:
  • 编辑TableView
  • 重载
    • hasUncommittedUpdates
    • reloadData
    • reloadRowsAtIndexPaths:withRowAnimation:
    • reloadSections:withRowAnimation:
    • reloadSectionIndexTitles
  • 视图区域
    • rectForSection:
    • rectForRowAtIndexPath:
  • 预加载
  • 索引配置
  • 通知
    • UITableViewSelectionDidChangeNotification

UITableView

  • SuperClass为UIScrollView

但只允许纵向滚动。

  • Section(节)

可以为0节、每个Section都有自己的Row
任何section都可以有选择地在前面的一个header,并且可以选择后跟一个footer

  • Row(行)

可以为0行、不过即使为0行该Sectionheader以及footer也会显示。

  • Style

有两种Style:UITableViewStylePlainUItableViewStyleGrouped。当你创建一个UITableView实例必须指定其的Style、并且无法改变。

UITableViewStylePlain

1.plain类型有多段时,段头停留(自带效果)
2.plain类型默认section之间没有中间的间距和头部间距(想让plain类型的section之间留有空白,需要在UITableView代理方法中return自定义的header和footer,并在自定义的UITableViewHeaderFooterView里面重写setFrame方法)

  1. 可以有索引存在
UItableViewStyleGrouped

sectionHeaderView不会悬浮、存在默认高度。

剩下诸如背景色什么的区别看这篇吧、反正都是定制基本不用考虑《iOS UITableView 的 Plain和Grouped样式的区别》


复用原理

可以参考一下:《UITableView的Cell复用原理和源码分析》
写的是mac_os的Chameleon、但原理应该都差不多。

简而言之:

  1. 用数组_cachedCells存储当前屏幕能展示的cell
    通过CGRectIntersectsRect(cellRect,visibleBounds)确定
  2. 滑动时、获取移出屏幕的cell
    _cachedCells进行copy、然后remove掉当前屏幕能展示的cell。剩下的cell就是消失的cell。
  3. 存入复用池
    将上一步获取的cell存入集合_reusableCells(当然我认为用字典也行)。
  4. 取出cell
    _reusableCells取出特定cell并且移出

至于是不是会在移出屏幕时从父视图remove掉、这个demo里是做了。但是真正的TableVIew似乎有一些不为人知的规则(大概率是总remove会造成性能损耗)、并不是一定会立刻remove。


UITableViewDataSource&&UITableViewDelegate

《iOS文档补完计划--UITableViewDataSource&&UITableViewDelegate》


配置TableView

  • style

返回初始化时设置的style

@property(nonatomic, readonly) UITableViewStyle style;

如果初始化时没有设置、默认为UITableViewStylePlain

  • - numberOfRowsInSection:

返回指定section中的row数

- (NSInteger)numberOfRowsInSection:(NSInteger)section;

在每次reload中、这个值会通过UITableViewDataSource获取并被缓存。

  • - numberOfSections

返回总计的section数

@property(nonatomic, readonly) NSInteger numberOfSections;

在每次reload中、这个值会通过UITableViewDataSource获取并被缓存。

  • rowHeight

单元格的统一高度

@property(nonatomic) CGFloat rowHeight;

一旦主动实现tableView:heightForRowAtIndexPath:这个值将会失效。并且在每一次显示cell时调用该方法、这样对性能会有一定影响。

默认值是UITableViewAutomaticDimension、也就是自适应。不过如果你使用ib生成控件、需要显示的设置rowHeight = UITableViewAutomaticDimension以自适应。

  • separator

分割线的样式、颜色、效果

//分割线样式 默认UITableViewCellSeparatorStyleSingleLine
@property(nonatomic) UITableViewCellSeparatorStyle separatorStyle;
//分割线颜色 默认`gray`
@property(nonatomic, strong) UIColor *separatorColor;
//分隔效果
@property(nonatomic, copy) UIVisualEffect *separatorEffect;

//分割线的位置
@property(nonatomic) UIEdgeInsets separatorInset;

//iOS11之后对separatorInset的扩展
@property(nonatomic) UITableViewSeparatorInsetReference separatorInsetReference;

不过通常我们都是自定义、所以直接
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;一劳永逸

  • backgroundView

TableView的背景View

@property(nonatomic, strong) UIView *backgroundView;

默认为nil。
你可以在一些情况下用到它、比如将一个图片作为backgroundView。
如果设置背景色直接使用backgroundView即可

  • cellLayoutMarginsFollowReadableWidth

pad开发时调整cell的margin用

@property(nonatomic) BOOL cellLayoutMarginsFollowReadableWidth;

在iPad开发时,使用iOS 9系统会出现tableviewCell的位置变化,在开发中默认与右侧是15个像素,可是现在明显大的多。需要将其置NO。


创建Cell

  • registerCell
//注册nib创建的cell
- (void)registerNib:(UINib *)nib 
forCellReuseIdentifier:(NSString *)identifier;

//注册代码创建的cell
- (void)registerClass:(Class)cellClass 
forCellReuseIdentifier:(NSString *)identifier;

你可以通过将Nib传入nil来复位注册表

  • - dequeueReusableCellWithIdentifier:forIndexPath:

返回已经注册的指定cell、并添加到tableView的指定位置中

- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier 
                                                   forIndexPath:(NSIndexPath *)indexPath;

复用池中有该cell则直接返回该cell、并调用cell的prepareForReuse方法
如果复用池中没有、则有两种情况:

  1. tableView中已经注册了cell
    直接新建cell、并调用cell的initWithStyle:reuseIdentifier:创建

  2. 没有注册cell
    直接崩溃这里要与dequeueReusableCellWithIdentifier:进行区分`。

  • - dequeueReusableCellWithIdentifier:

返回指定的id的cell

- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

和上一个方法基本相同。

  • 二者的异同
相同点:
  1. 二者首先都会去从重用池(cellForRow返回的cell在移出屏幕时会被加入重用池)查找。
  2. 如果重用池中没有、再尝试从注册表查找。
不同点:
  1. 如果没有注册、他会返回nil。而前者会崩溃
  2. 他只是取出cell、前者会附加一个添加到TableView的操作(因为必然返回cell吧?)。

页眉和页脚

页眉页脚的注册、提取、插入。

//注册
- (void)registerNib:(nullable UINib *)nib forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
- (void)registerClass:(nullable Class)aClass forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);

//获取
- (nullable __kindof UITableViewHeaderFooterView *)dequeueReusableHeaderFooterViewWithIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0); 

//页眉同一高度
@property(nonatomic) CGFloat sectionHeaderHeight;

//页脚同一高度
@property(nonatomic) CGFloat sectionFooterHeight;


//整个TableView的页眉
@property(nonatomic, strong) UIView *tableHeaderView;

//整个TableView的页脚
@property(nonatomic, strong) UIView *tableFooterView;

Cell&&IndexPath

  • - cellForRowAtIndexPath:

返回指定indexPath位置的cell

- (__kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath;

有两种情况会返回nil

  1. 已经进入复用池
  2. 超出范围的indexPath
  • - indexPathForCell:

返回指定cell的indexPath索引

- (NSIndexPath *)indexPathForCell:(UITableViewCell *)cell;

如果cell已经进入复用池、也会返回nil

  • - indexPathForRowAtPoint:

返回指定的点所在的IndexPath

- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point;

超出了TableVIew的bounds会返回nil

  • - indexPathsForRowsInRect:

返回rect所包含的IndexPath

- (NSArray<NSIndexPath *> *)indexPathsForRowsInRect:(CGRect)rect;
  • - visibleCells

返回当前可见的所有cell

@property(nonatomic, readonly) NSArray<__kindof UITableViewCell *> *visibleCells;
  • - indexPathsForVisibleRows

返回所有可见的IndexPath

@property(nonatomic, readonly) NSArray<NSIndexPath *> *indexPathsForVisibleRows;

预估高度

Cell、Header、Footer

@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); 
@property (nonatomic) CGFloat estimatedSectionHeaderHeight NS_AVAILABLE_IOS(7_0); 
@property (nonatomic) CGFloat estimatedSectionFooterHeight NS_AVAILABLE_IOS(7_0); 

默认UITableViewAutomaticDimension、设置成0则禁用。

这里有一些需要注意的地方:
  1. 禁用预估功能
    tableView会每次都读取所有indexPathheightForRowAtIndexPath进行行高计算。

  2. 开启预估功能
    rectForSection方法返回的rect也为预估值。只有当你划过所有的cell、才会得到真实的值。


滚动tableView

  • - scrollToRowAtIndexPath:atScrollPosition:animated:

将TableView的某个IndexPath滚动到特定位置

- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath 
              atScrollPosition:(UITableViewScrollPosition)scrollPosition 
                      animated:(BOOL)animated;

UITableViewScrollPosition为一个枚举类型、表示滚动到屏幕的何处

typedef NS_ENUM(NSInteger, UITableViewScrollPosition) {
    UITableViewScrollPositionNone,//只保证能出现在屏幕上
    UITableViewScrollPositionTop,    //上
    UITableViewScrollPositionMiddle,   //中
    UITableViewScrollPositionBottom//下
}; 

文档上说该方法不会引起scrollViewDidScroll:的调用、但我测试是会调用的。存疑吧

  • - scrollToNearestSelectedRowAtScrollPosition:animated:

让选定的行滚动到屏幕指定位置

- (void)scrollToNearestSelectedRowAtScrollPosition:(UITableViewScrollPosition)scrollPosition 
                                          animated:(BOOL)animated;

比如自己实现一个滚轮、点击的cell滚动到中间。


选择控制

  • indexPathForSelectedRow

返回所选行的IndexPath

@property(nonatomic, readonly) NSIndexPath *indexPathForSelectedRow;

如果有多个选择、则返回第一个

  • indexPathsForSelectedRows

多选用

@property(nonatomic, readonly) NSArray<NSIndexPath *> *indexPathsForSelectedRows;
  • - selectRowAtIndexPath:animated:scrollPosition:

选择指定行、可以有动画

- (void)selectRowAtIndexPath:(NSIndexPath *)indexPath 
                    animated:(BOOL)animated 
              scrollPosition:(UITableViewScrollPosition)scrollPosition;

这个方法不会引起tableView:willSelectRowAtIndexPath:tableView:didSelectRowAtIndexPath:UITableViewSelectionDidChangeNotification的调用。

如果传递UITableViewScrollPositionNone将会导不发生滚动。如果想要这种效果、你需要自己实现滚动代码scrollToRowAtIndexPath:atScrollPosition:animated:

  • - deselectRowAtIndexPath:animated:

取消选择、可以有动画

- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath 
                      animated:(BOOL)animated;

与上一个方法一样、它也不会导致一些相关方法被调用。(tableView:willDeselectRowAtIndexPath:tableView:didDeselectRowAtIndexPath:UITableViewSelectionDidChangeNotification)
并且不会产生任何滚动效果。

  • 选择权限

普通选择、编辑选择与多选

//正常模式下是否允许选择。默认YES
@property(nonatomic) BOOL allowsSelection
//正常模式下的多行选择。默认NO。可以与indexPathsForSelectedRows配合使用
@property(nonatomic) BOOL allowsMultipleSelection;
//编辑模式下是否允许选择。默认YES
@property(nonatomic) BOOL allowsSelectionDuringEditing;
//编辑模式下多选。默认NO
@property(nonatomic) BOOL allowsMultipleSelectionDuringEditing;

插入&&删除&&移动

一下方法会导致TableView会立即调用相关代理方法、以获取内容。

同时、我们可以批量操作

- (void)testTableViewUpdateCrash {
    NSIndexPath *insertIndexPath = [NSIndexPath indexPathForRow:10 inSection:0];
    NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForRow:11 inSection:0];
    NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForRow:12 inSection:0];
    NSIndexPath *moved1IndexPath = [NSIndexPath indexPathForRow:13 inSection:0];
    NSIndexPath *moved2IndexPath = [NSIndexPath indexPathForRow:14 inSection:0];
    [self.tableView beginUpdates];
    [self.tableView insertRowsAtIndexPaths:@[insertIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.tableView deleteRowsAtIndexPaths:@[deleteIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.tableView reloadRowsAtIndexPaths:@[reloadIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.tableView moveRowAtIndexPath:moved1IndexPath toIndexPath:moved2IndexPath];
    [self.tableView endUpdates];
}

需要注意的是beginUpdatesenpdate是串行的。

所以、我们最好先操作数据源然后再进行操作。

  • - insertRowsAtIndexPaths:withRowAnimation:

插入。可以有动画效果

//插入
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths 
              withRowAnimation:(UITableViewRowAnimation)animation;

//删除
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths 
              withRowAnimation:(UITableViewRowAnimation)animation;

//移动-不允许动画、每次移动单行
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath 
               toIndexPath:(NSIndexPath *)newIndexPath;

//插入sections
- (void)insertSections:(NSIndexSet *)sections 
      withRowAnimation:(UITableViewRowAnimation)animation;

//删除sections
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
//移动sections
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;
  • - performBatchUpdates:completion:

将多个操作放入动画组进行。iOS11之后

- (void)performBatchUpdates:(void (^)(void))updates 
                 completion:(void (^)(BOOL finished))completion;
  • - beginUpdates&&endUpdates

准备一系列插入、删除或选择等操作

- (void)beginUpdates;
- (void)endUpdates;
  1. 尽量使用performBatchUpdates:completion:而不是这个方法

编辑TableView

修改TableView的编辑状态

//属性版
@property(nonatomic, getter=isEditing) BOOL editing;
//动画版
- (void)setEditing:(BOOL)editing 
          animated:(BOOL)animated;

具体使用可以参阅《iOS文档补完计划--UITableViewDataSource&&UITableViewDelegate》中关于插入、删除、拖动的实现。


重载

  • hasUncommittedUpdates

Drag & Drop相关。当返回YES时不建议进行reload

@property(nonatomic, readonly) BOOL hasUncommittedUpdates;
  • - reloadData

重载整个TableVIew

- (void)reloadData;

不应该在插入或删除行的方法中调用,特别是在用beginUpdates和enpdate调用实现的动画块中。

  • - reloadRowsAtIndexPaths:withRowAnimation:

允许使用动画重载指定的indexPath

- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths 
              withRowAnimation:(UITableViewRowAnimation)animation;
  • - reloadSections:withRowAnimation:

重载指定的section

- (void)reloadSections:(NSIndexSet *)sections 
      withRowAnimation:(UITableViewRowAnimation)animation;
  1. 如果指定了可视的IndexPath。这个方法会立即向数据源请求新的单元格。
  2. 新cell执行插入动画,旧cell执行移出动画
  • - reloadSectionIndexTitles

刷新右侧索引

- (void)reloadSectionIndexTitles;

视图区域

  • - rectForSection:

返回指定section所占的范围

- (CGRect)rectForSection:(NSInteger)section;

需要注意这个方法的准确性、取决于预估行高如何去设置。

  • - rectForRowAtIndexPath:

返回指定index所占的范围

- (CGRect)rectForRowAtIndexPath:(NSIndexPath *)indexPath;

如果该cell还没有被展示过、系统会去获取正确的高度。(也就是调用heightForRowAtIndexPath甚至cellForRowAtIndexPath)

indexPath违规则返回CGRectZero

还有两个方法、懒得测试了...

- (CGRect)rectForFooterInSection:(NSInteger)section;
- (CGRect)rectForHeaderInSection:(NSInteger)section;

预加载

  • prefetchDataSourc

通过Pre-Fetching功能预知用户行为

@property(nonatomic, weak) id<UITableViewDataSourcePrefetching> prefetchDataSource;

这个协议其实是用来通知我们,当前滑动到某个区域后,根据这次滑动的方向接下去可能还会滑向哪些indexPaths。好让我们做一些数据上的预备或者销毁。

单个id的复用池为队列reusable-cell queue.


索引配置

索引行数

@property (nonatomic) NSInteger sectionIndexMinimumDisplayRowCount;                                                      // 当行数达到某个数值,则显示索引栏
@property (nonatomic, strong, nullable) UIColor *sectionIndexColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;                   // 索引颜色
@property (nonatomic, strong, nullable) UIColor *sectionIndexBackgroundColor NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR;         // 正常情况下的背景色
@property (nonatomic, strong, nullable) UIColor *sectionIndexTrackingBackgroundColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR; // 触摸时的背景色

通知

  • UITableViewSelectionDidChangeNotification

选择项发生更改时发送通知

const NSNotificationName UITableViewSelectionDidChangeNotification;

参考资料

官方文档--UITableView
iOS_10中UICollectionView和UITableView的变化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352