本文旨在帮助各位大佬复习巩固基础知识。
一、tableView类型与系统自带cell类型
二、两种注册系统cell的方法
三、常用代理方法总结
四、实战练习
五、巩固提高-两个tableView的联动
六、感谢参考文章 与本文demo链接
一、tableView类型与系统自带cell类型
1、tableView样式可分为普通样式与分区样式两种:
UITableViewStylePlain、UITableViewStyleGrouped
两者的区别在于一个grouped样式可以有分区头,而plain样式则没有
2、cell样式可分为以下四种:
UITableViewCellStyleValue1、UITableViewCellStyleValue2、UITableViewCellStyleDefault、UITableViewCellStyleSubtitle
四种cell样式的区别在于imageView、textLabel、detailTextLabel、accessoryType 的位置、显示不同。
如图:
二、两种注册系统cell的方法
cell注册目的都在于cell的复用来节省开支。
1、常用的,在代理行为返回cell时进行注册(据了解:系统推荐用这个方法,这个方法在某些情况下更加能避免循环复用)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// kPlaceHolderCellId是我定义的一个宏字符串
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kPlaceHolderCellId];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:(UITableViewCellStyleValue1) reuseIdentifier:kPlaceHolderCellId];
}
return cell;
}
2、在tableView申请之后进行注册(这样在cellForRowAtIndexPath里就不需要对cell 是否为空进行判断)
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kPlaceHolderCellId];
// 下面是代理方法,
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kPlaceHolderCellId];
return cell;
}
本文不对xib注册进行解释,详情可查看其他朋友文章。
三、常用代理方法总结
1、数据源:
// 返回第section组中有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
// 返回多少组,没实现该方法,默认为1
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
2、UITableViewDelegate代理方法
// 即将显示tableviewcell时调用
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
// 即将显示header时调用,在cell之后调用
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);
// 即将显示footer时调用,在header之后调用
- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);
// 在删除cell之后调用,停止显示cell的时候调用,界面不显示cell时。
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath NS_AVAILABLE_IOS(6_0);
// 停止显示header的时候调用
- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0);
// 停止显示footer的时候调用
- (void)tableView:(UITableView *)tableView didEndDisplayingFooterView:(UIView *)view forSection:(NSInteger)section NS_AVAILABLE_IOS(6_0)
3、操作cell时调用的方法
// cell选中时调用
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
// cell取消选中时调用
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(3_0);
4、设置分组view的方法
// 返回某个section对应的header标题
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;
// 返回某个section对应的footer标题
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section;
// 设置第section分组的header
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;
// 设置第section分组的footer
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;
5、高度的代理方法
// 在设置每行cell的高度,header的高度,footer的高度
// 设置某行cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
// 设置header高度
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
// 设置footer高度
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;
6、编辑模式相关的代理方法
// 返回每一行cell的编辑模式, 可以再次设置add或者删除操作。
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath;
// cell左滑删除时,删除按钮的标题
- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(3_0);
// 自定义编辑左滑后出现的界面。 不止只有一个delete按钮, 可以自行定义,返回一个数组。数组中放着UITableviewRowAction
- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0);
// 未实现 默认为yes,进入编辑时,cell是否缩进。 在开启编辑状态时调用。
- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath;
// 右滑准备进行编辑的时候 调用。 将setediting = yes时不调用
- (void)tableView:(UITableView*)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath;
// 完成编辑的时候调用
- (void)tableView:(UITableView*)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath;
7、索引
//返回要显示的section索引标题
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView;
// return list of section titles to display in section index view (e.g. "ABCD...Z#")
// 点击右侧索引表项时调用
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index;
// 返回指定点所在位置的indexPath
- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point;
// 返回指定cell所在的indexPath
- (NSIndexPath *)indexPathForCell:(UITableViewCell *)cell;
// 返回指定范围rect中的所有cell的indexPath
- (NSArray *)indexPathsForRowsInRect:(CGRect)rect;
// returns nil if rect not valid
// 返回索引indexPath所指向的cell。
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath;
8、tableview的滚动方法
// 根据传入的indexPath,滚动到相对应的位置,第二个参数是控制对应的cell再滚动后处于tableview的顶部/底部/中部等
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
// 滚动到被选中项。 滚动后处于tableview的顶部/底部/中部等
- (void)scrollToNearestSelectedRowAtScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
9、插入,删除,刷新,移动section组
// 插入,删除,刷新,移动section组
// 插入section
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
// 删除section
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
// 刷新section
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation: (UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);
// 移动section
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection NS_AVAILABLE_IOS(5_0);
四、实战练习
1、自定义cell
效果图:
1)、通过自定义cell可以实现各式各样的我们想要的样式,当cell定义后,如果我们暴露在外面的是label,则可以通过改变label的text属性来进行赋值,如果暴露到外面的是string,想通过string赋值来改变label的话,我使用的是rac进行监听string的变化。
2)、cell的定义可以通过高内聚来实现数据与controller的分离。
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell;
if (indexPath.row%2 == 0) {
// 通过cell的事件方法处理,将cell的定义在cell定义中进行自己的实现,这样就可以达到高内聚低耦合。 下面的model是定义的一种数据类型,这样congtroller也不需要管具体数据是什么了
OneTableViewCell *oneCell = [OneTableViewCell createCellWithTableView:tableView];
oneCell.myModel = _model;
cell = oneCell;
}else{
// 通过传字符串处理,这个cell暴露在外面的string。我在cell实现中进行了rac监听
TwoTableViewCell *oneCell = [tableView dequeueReusableCellWithIdentifier:kPlaceHolderCellId2];
if (oneCell == nil) {
oneCell = [[TwoTableViewCell alloc]initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:kPlaceHolderCellId2];
}
oneCell.nameStr = _nameArr[indexPath.row];
oneCell.messageStr = _messageArr[indexPath.row];
cell = oneCell;
}
return cell;
}
第一个cell
// 类方法用来作为cell对外的调用接口
+(instancetype)createCellWithTableView:(UITableView *)tableView{
OneTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kPlaceHolderCellId];
if (cell == nil) {
cell = [[OneTableViewCell alloc]initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:kPlaceHolderCellId];
}
return cell;
}
// 实现cell布局
-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
id headerView = ImageView.img(@"header2.jpg").borderRadius(40);
_nameLabel = Label.fitSize.str(_myModel.name);
_messageLabel = Label.fitSize.str(_myModel.message);
id messageView = View;
id verS1 = VerStack(@5,
_nameLabel,
_messageLabel,).gap(5).embedIn(messageView);
id verS2 = VerStack(@5,
headerView,
@5);
HorStack(@5,verS2,verS1,NERSpring).gap(15).embedIn(self);
}
return self;
}
// model赋值
-(void)setMyModel:(YSCellModel *)myModel{
_myModel = myModel;
self.nameLabel.text = _myModel.name;
self.messageLabel.text = _myModel.message;
NSLog(@"执行了,%@",_myModel.name);
}
第二个cell
-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
id headerView = ImageView.img(@"header4.jpg").borderRadius(40);
UILabel *nameLabel = Label.fitSize.str(_nameStr);
UILabel *speakLabel = Label.fitSize.str(_messageStr);
id messageView = View;
id verS1 = VerStack(@5,
nameLabel,
speakLabel,).gap(5).embedIn(messageView);
id verS2 = VerStack(@5,
headerView,
@5);
HorStack(@5,verS2,verS1,NERSpring).gap(15).embedIn(self);
// 通过rac监听string的改变来改变label的text
[RACObserve(self, self.nameStr) subscribeNext:^(id _Nullable x) {
nameLabel.text = x;
}];
[RACObserve(self, messageStr) subscribeNext:^(id _Nullable x) {
speakLabel.text = x;
}];
}
return self;
}
2、cell的侧滑按钮-cell的移动与编辑状态
效果图:
1)、cell的侧滑按钮可通过editActionsForRowAtIndexPath行为返回的数组来进行定义。
2)、cell的移动需要在编辑状态下实现canMoveRowAtIndexPath 和 moveRowAtIndexPath两个方法。
cell的侧滑实现
// 自定义编辑左滑后出现的界面。 不止只有一个delete按钮, 可以自行定义,返回一个数组。数组中放着UITableviewRowAction
-(NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewRowAction *action0 = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"关注" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
NSLog(@"点击了关注");
// 收回左滑出现的按钮(退出编辑模式)
tableView.editing = NO;
}];
UITableViewRowAction *action1 = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"删除" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
[self.myArray removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
return @[action1, action0];
}
cell的编辑状态下的移动
// 在编辑状态下可以移动
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath{
return YES;
}
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
// demo中并没有在这里操作,如果你不想退出程序后变回原样的话,就在这里对你的数据数组进行换位置
}
cell编辑的样式
// 返回cell支持的编辑样式 可以自己尝试查看每种样式的样子
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
//表示支持默认操作
//return UITableViewCellEditingStyleNone;
//表示支持删除操作
// return UITableViewCellEditingStyleDelete;
//表示支持新增操作
// return UITableViewCellEditingStyleInsert;
return UITableViewCellEditingStyleDelete ;
}
3、mjrefresh的刷新
关于这个第三方库的使用,网上有蛮多教程的。这里只介绍一个切换图片的类似动图的刷新状态。
效果图(gif有点卡):
代码
MJRefreshGifHeader *header = [MJRefreshGifHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewData)];
NSArray *test = [NSArray arrayWithObjects:Img(@"header4.jpg"),Img(@"header2.jpg"), nil];
// 注意,只有隐藏了下面两个状态栏才能让图片在居中显示。如果你还希望自定义文字可查看demo,也可自己上网八一八更详细的资料
// 隐藏时间
header.lastUpdatedTimeLabel.hidden = YES;
// 隐藏状态栏
header.stateLabel.hidden = YES;
4、多分区的删除cell - 索引
效果图:
1)、之所以单独拿出来讲是因为我这种菜鸡之前也不会,删除时候总是崩溃,所以总结了两种方法来删除多分区的cell。(demo中没有对一个分区中只有一个cell的情况进行适配)
2)、索引需要实现sectionIndexTitlesForTableView 和 sectionForSectionIndexTitle两个方法就可以了
删除cell
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
// 首先获取对应分区的数组 然后删除数组中你删除的cell
NSString *str = self.allKeys[indexPath.section];
self.dataArray = [self.dictionary objectForKey:str];
[self.dataArray removeObjectAtIndex:indexPath.row];
if (self.dataArray.count == 0) {
[self.dictionary removeObjectForKey:str];
self.allKeys = [self.dictionary allKeys];
}
// 这样重新加载不能带动画的删除 这就是方法1
// [self.tableView reloadData];
// 这样可以带动画的删除cell 这样就是方法2
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationFade)];
}
索引
//返回索引数组
-(NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView{
return [self.allKeys sortedArrayUsingSelector:@selector(compare:)];
}
//响应点击索引时的委托方法
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{
NSInteger count = 0;
for (NSString *character in self.allKeys) {
if ([[character uppercaseString] hasPrefix:title]) {
return count;
}
count++;
}
return 0;
}
五、巩固提高-两个tableView的联动
tableview的联动有两种方式,有人说使用willDisplayHeaderView 和 didEndDisplayingHeaderView 方法会出现联动不准确的bug,也许我神经比价大条并没有感觉会有不准确的情况,不过第二种方法代码量确实较少。仁者见仁。以下为两种方法的实现方式
方法一
// 此处省略leftTable 和 rightTable的声明 和 数据的声明过程,详细代码稍后会发demo出来
#pragma mark 方法一
// 点击左侧可滚动右侧
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (tableView == _leftTabele) {
NSInteger select = indexPath.row;
[_rightTable scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:select] atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
// 标记右侧tableVIew是向下滚动还是向上滚动
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.leftTabele) return;
static CGFloat lastOffsetY = 0;
UITableView *tableview = (UITableView *)scrollView;
if (tableview == _rightTable) {
_isScrollDown = lastOffsetY < scrollView.contentOffset.y; // 手指向上拖动就为真,向下拖动就为假(界面向上滚动、界面向下滚动)
lastOffsetY = scrollView.contentOffset.y; // 记录当前y值,向下变大,向上变小
// NSLog(@"%@",_isScrollDown ? @"1":@"2");
}
}
// 即将展示头部标题 手指向下拖动的时候才显示这个
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section{
// 当前tableView 是右侧的, 滑动方向向下,是用户拖拽产生的滚动
if ((tableView == _rightTable) && !_isScrollDown && _rightTable.dragging) {
NSLog(@"分区将要显示:%ld",section);
[self selectRowAtIndexPath:section];
}
}
// 分区标题展示结束 手指向上滑动的时候显示这个
-(void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section{
// 当钱tableView 是右侧的,滚动方向是向上,是用户拖拽产生的滚动
NSLog(@"t2:%d",_rightTable.dragging);
if ((tableView == _rightTable) && _isScrollDown && _rightTable.dragging) {
NSLog(@"分区将要隐藏:%ld",section);
[self selectRowAtIndexPath:section];
}
}
// 当拖动右边 tableView的时候,处理左边tableView
-(void)selectRowAtIndexPath:(NSInteger)index{
[_leftTabele selectRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0] animated:YES scrollPosition:(UITableViewScrollPositionTop)];
}
方法二
#pragma mark 方法二
// 此处省略leftTable 和 rightTable的声明 和 数据的声明过程,详细代码稍后会发demo出来
// 点击左侧可滚动右侧
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (tableView == _leftTabele) {
NSInteger select = indexPath.row;
[_rightTable scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:select] atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
// 标记右侧tableVIew是向下滚动还是向上滚动
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.leftTabele) return;
NSLog(@"这里是视图上所有cell:%@",[self.rightTable indexPathsForVisibleRows]);
NSLog(@"这里是最考上的一个cell:%@",[[self.rightTable indexPathsForVisibleRows] firstObject]);
// 取出显示在 视图 且最靠上 的 cell 的 indexPath
NSIndexPath *topHeaderViewIndexpath = [[self.rightTable indexPathsForVisibleRows] firstObject];
// 左侧 talbelView 移动到的位置 indexPath
NSIndexPath *moveToIndexpath = [NSIndexPath indexPathForRow:topHeaderViewIndexpath.section inSection:0];
// 移动 左侧 tableView 到 指定 indexPath 居中显示
[self.leftTabele selectRowAtIndexPath:moveToIndexpath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
}
方法二解析图(图片来源于网络)
我遇到的bug
如图:
bug描述:图片要展示的bug是,当滑动右侧tableView 的时候会直接从A分区跳到N分区,无论上拉还是下拉。后来解决这个bug,是因为我用的MAC外接了一个显示器,当使用快捷键ctrol+option进行快速屏幕拖动时出发了模拟器的option选项,从而导致了这个bug。解决办法就是在模拟器上再点击的时候再按住option就可以了。
鸣谢文章链接
iOS两个 TableView 联动. 思路简单明了,无bug!