移动端技术

IOS开发之浅谈UITableViewCell

2018-08-31  本文已影响4人  默客_78ec

在IOS开发当中我们最常用的控件当然要数UITableVIew了,下面是我对这个控件的一些理解和看法,这里我们需要关注_cachedCells, _sections, _reusableCells 这三个变量的作用。

下面是 dataSrouce 的 setter 方法源码:

- (void)setDataSource:(id)newSource

{

    _dataSource = newSource;

    _dataSourceHas.numberOfSectionsInTableView = [_dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)];

    _dataSourceHas.titleForHeaderInSection = [_dataSource respondsToSelector:@selector(tableView:titleForHeaderInSection:)];

    _dataSourceHas.titleForFooterInSection = [_dataSource respondsToSelector:@selector(tableView:titleForFooterInSection:)];

    _dataSourceHas.commitEditingStyle = [_dataSource respondsToSelector:@selector(tableView:commitEditingStyle:forRowAtIndexPath:)];

    _dataSourceHas.canEditRowAtIndexPath = [_dataSource respondsToSelector:@selector(tableView:canEditRowAtIndexPath:)];

    [self _setNeedsReload];

}

下面是 delegate 的 setter 方法源码:

- (void)setDelegate:(id)newDelegate

{

    [super setDelegate:newDelegate];

    _delegateHas.heightForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)];

    _delegateHas.heightForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:heightForHeaderInSection:)];

    _delegateHas.heightForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:heightForFooterInSection:)];

    _delegateHas.viewForHeaderInSection = [newDelegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)];

    _delegateHas.viewForFooterInSection = [newDelegate respondsToSelector:@selector(tableView:viewForFooterInSection:)];

    _delegateHas.willSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)];

    _delegateHas.didSelectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)];

    _delegateHas.willDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)];

    _delegateHas.didDeselectRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)];

    _delegateHas.willBeginEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:willBeginEditingRowAtIndexPath:)];

    _delegateHas.didEndEditingRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:didEndEditingRowAtIndexPath:)];

    _delegateHas.titleForDeleteConfirmationButtonForRowAtIndexPath = [newDelegate respondsToSelector:@selector(tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:)];

}

根据UITableVIew底层Delegate和DataSource执行方法,我们可以看出在添加UITableVIew之后,需要实现的方法和协议方法执行的顺序。

同时在实现的过程当中使用了三个容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的复用,这是 UITableView 最核心的地方。

下面一起看看三个容器在创建到滚动整个过程中所包含的元素的变化情况。

在第一次设置了数据源调用该方法时,三个容器的内容都为空,在调用完该方法后 _cachedCells 包含了当前所有可视 Cell 与其对应的indexPath 的键值对,availableCells 与 _reusableCells 仍然为空。只有在滚动起来后 _reusableCells 中才会出现多余的未显示可复用的 Cell。

刚创建 UITableView 时的状态如下图(红色为屏幕内容即可视区域,蓝色为超出屏幕的内容,即不可视区域):

image

如图,当前 _cachedCells 的元素为当前可视的所有 Cell 与其对应的 indexPath 的键值对。向上滚动一个 Cell 的过程中,由于 availableCells 为 _cachedCells 的拷贝,所以可根据 indexPath 直接取到对应的 Cell,这时从底部滚上来的第7行,由于之前的 _reusableCells 为空,所以该 Cell 是直接创建的而并非复用的,由于顶部 Cell 滚动出了可视区域,所以被加入了 _reusableCells 中以便后续滚动复用。滚动完一行后的状态变为了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滚动出可视区域的第一行 Cell 的引用。

image

当向上滚动两个 Cell 的过程中,同理第 3 行到第 7 行的 Cell 可以通过对应的 indexPath 从 _cachedCells 中获取。这时 _reusableCells 中正好有一个可以复用的 Cell 用来从底部滚动上来的第 8 行。滚动出顶部的第 2 行 Cell 被加入 _reusableCells 中。

image

UITableView高度自适应

在UITableView列表的使用中,因为在自定义的UITableViewCell中页面相对复杂,所以会出现每一个cell都有不同的高度。这时候就需要根据实际内容进行cell的更新约束,其实说到底也就是哪些UI子视图应该显示,或隐藏,哪些UILabel标签高度是这个数值,哪些UILabel标签的高度是那个数值。

这样想的话,我们在研发时就可以根据实际的数据Model进行控制UI的显示,或隐藏,也就是更新UI的约束,以便设置UI的自适应显示;其次再计算出实际的高度用于在UITableView的代理回调方法中设置cell的高度。

先看下效果图

image

下面是实现的一部分代码

#import 

@interface TableViewModel : NSObject

@property (nonatomic, strong) NSString *title;

@property (nonatomic, strong) NSString *content;

@property (nonatomic, strong) NSString *imageName;

@property (nonatomic, assign) CGFloat height;

@end
#import "TableViewModel.h"
#import "TableViewCell.h"
 
@implementation TableViewModel
 
- (CGFloat)height
{
    if (!_height)
    {
        // 调用cell的方法计算出高度
        _height = [TableViewCell heightTableCellWithModel:self];
    }
    
    return _height;
    
}
 
@end

#import "TableViewViewController.h"
#import "TableViewModel.h"
#import "TableViewCell.h"
 
@interface TableViewViewController () <UITableViewDataSource, UITableViewDelegate>
 
@property (nonatomic, strong) UITableView *mainTableView;
@property (nonatomic, strong) NSArray *array;
 
@end
 
@implementation TableViewViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    self.title = @"tableview";
    
    [self setUI];
}
 
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
 
#pragma mark - 视图
 
- (void)setUI
{
    [self.mainTableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.equalTo(self.view);
    }];
}
 
#pragma mark - UITableViewDataSource, UITableViewDelegate
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.array.count;
}
 
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    TableViewModel *model = self.array[indexPath.row];
    CGFloat height = model.height;
    
    NSLog(@"index = %ld, height:%@", indexPath.row, @(height));
    return height;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    TableViewCell *cell = (TableViewCell *)[tableView dequeueReusableCellWithIdentifier:identifierTableViewCell];
    if (cell == nil)
    {
        cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifierTableViewCell];
    }
    
    TableViewModel *model = self.array[indexPath.row];
    cell.model = model;    
    
    return cell;
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    TableViewModel *model = self.array[indexPath.row];
    NSLog(@"index = %ld, height = %@", indexPath.row, @(model.height));
}
 
#pragma mark - getter
 
- (UITableView *)mainTableView
{
    if (!_mainTableView)
    {
        _mainTableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
        [self.view addSubview:_mainTableView];
 
        _mainTableView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:0.3];
        _mainTableView.delegate = self;
        _mainTableView.dataSource = self;
        
        // 设置为动态高度
        _mainTableView.estimatedRowHeight = UITableViewAutomaticDimension;
    }
    
    return _mainTableView;
}
 
- (NSArray *)array
{
    if (!_array)
    {
        NSArray *tmpArray = @[@"hello,我是iOS开发菜鸟。",@"很多iOS开发者应该都用过autolayout,如果用故事版和XIB的话非常好用,但是如果用纯代码的方式写的话就感觉这东西太啰嗦了,一点都不好用,还不如frame来得快,然而在公司项目中一般都是多人开发,因此还是以纯代码写的方式比较多。",@"一个国外大神推出了一套封装好autolayout框架Masnory,Masonry是一个轻量级的布局框架拥有自己的描述语法采用更优雅的链式语法封装自动布局简洁明了并具有高可读性而且同时支持iOS和Max OS X。",@"银行不改变,就让支付宝就改变银行,至少让银行睡不着。",@"其实押金池这个是有很多争议的,押金金融确实是令很多人高潮,也是摩拜被人议论得最多的一种盈利模式。押金池的本质是滚雪球,每个进来的用户都要交299块押金,那么这些押金是否可以用于下一批单车的采购,然后投放到其他城市,接着又有人用,又有押金,如此滚雪球似的增长。但是如果是这样的话,那么很多行业都有押金机制,那是否都可以这样玩呢?",@"其实在很多外行看来,几千万的注册量确实是很厉害,但是这个注册量的月活流量真的不怎么值得去关注,毕竟像摩拜这样的大公司,这些广告费只能是算做一个外快钱,并不能作为支持企业活下去。",@"政府自己就有公益车,有什么理由要补贴你,不竞争就不错了吧?政府关系参考滴滴至今,大谈这是优势的人,应该再考虑考虑。",@"类似广告,现阶段只是快钱,单车背后的人群,量级,产品回报率,这些东西究竟能在广告主品牌主值多少钱,打问号。",@"租金就是用户租用单车平台所得到的钱,一小时一块钱,那这样必须得有很高的使用频率才能够获取高的租金,但是摩拜其运营就是一个无底洞,单单这些租金恐怕是成本也很难赚回来。",@"融资仍然是摩拜如今最赚钱的手段,在1月4好,摩拜已经完成了D轮融资,融资金额为2.15亿美元,摩拜就是在不断的融资中拓展了其资金链,使其能够继续的生存下去。",@"有人分析出来摩拜利用GPS来分析用户的具体行为,通过统计用户的行为包装成数据卖给需要的大企业,但是这些数据具体参考价值也是用户的活跃地区在哪几个地方而已,具体价值还是有待研究的。",@"晚上跟公司领导和同事打麻将,散场后回到家,老婆问怎么样?我苦着脸说:“跟领导打牌能赢吗?输了五百多。”老婆听后点了点头,说:“下次别去打了,这次的钱就不从你下个月零用钱里扣了。”我爽快的答应了。第二天和老婆去菜场买菜,正巧遇到领导老婆,那老娘们一开口:“小律啊,你昨天手气真是太好了!一摞三,赢了一千多啊!”额,我特么差点没被这句话给噎死....。",@"表姐大龄剩女一枚,昨天有人给她介绍对象,表姐去了。见面地点约在一家餐厅,男人又矮又丑,表姐看不上。“本来我想走的,后来……”表姐没说完就害羞起来……我问:后来你被他的言谈吸引没走?表姐:他都走了,还点了一桌子菜,我怕浪费……",@"一天李靖对哪吒说,儿啊,你知道我为何总是对你不满吗?哪吒说难道是因为我太调皮了吗。李靖一巴掌就扇了上去,你个小逼崽子在你娘肚子里一待就是三年,急得为父手都能托塔了!哪吒母亲一笑说,怪不得塔下面有一个洞呢。",@"我四肢发达,好斗,一次在学校打伤一同学,去医院花了好多钱,不敢跟家里讲,于是找同学借,一同班女同学借给我最多……一段时间后,她没钱吃饭,催我还钱,我没办法,天天带她到朋友那里蹭饭……不久,她就变成我女朋友了…………终于,她将我带到她家见父母,居然遇到被我打伤的那个学生,他开口说:姐夫好!。",@"罗纳尔迪尼奥,个人荣誉:世界足球先生(2004,2005),金球奖(2005),南美足球先生(2013),金足奖(2009),国家队荣誉:美洲杯冠军(1999),世界杯冠军(2002),联合会杯冠军(2005),俱乐部荣誉:西甲冠军(2004,2005),欧洲冠军联赛冠军(2006),南美解放者杯冠军(2013),西班牙超级杯冠军(2004,2005)。",@"自从360免费后,一下子电脑的病毒似乎都消失了。",@"周鸿祎在一次节目上透露开始做360时,自己把别人卖到200元的软件,自己卖到25元,发现还是做不过别人。因为自己不是第一个做了,可能是第四个,第五个,市场都被别人占领了。",@"哪怕改变一个模式也是重新。",@"你现在做什么行业呢?是否能把自己卖的东西和360一样做到免费,而从其他地方赚钱?这是给每个创业者启发的。",@"一位锤子科技前员工在微博上发布长文《我为什么离开锤子科技》,向网友公开他离开锤子科技的原因。这位锤子科技前员工主要叙述了三个问题:一个是对上级的管理方式的不满,经常催促任务和越过中间级别布置详细任务;另一个是公司福利减少,没有中秋红包;最后一个是锤子科技的所谓弹性工作制度的问题,作者认为“所谓的弹性工作制就是一种可以让你每天加班到10点但是不用付薪水的制度。",@"马云曾经说过:“员工的离职原因很多,只有两点最真实: 1、钱,没给到位; 2、心,委屈了。”",@"华为的薪酬主要包括三部分:工资、奖金和分红;当然如若外派国外,还有外派补助+艰苦补助。华为的这种高薪政策及配“股票”的政策让员工极具“主人翁”意识:在公司大发展时,一起享受公司发展带来的红利;在公司困难时,能迎难而上与公司同舟共济。",@"华为不仅“舍得花钱”,更重要是“懂得分钱”,设计了一套“定岗定薪,易岗易薪”,意思就是工资薪酬是根据岗位来设定。",@"很多时候我们过高估计了机遇的力量,低估了规划的重要性,不明确的乐观主义者只知道未来越来越好,却不知道究竟多好,因此不去制定具体计划。他想在未来获利,但是却认为没有必要制定具体规划。"];
        
        NSMutableArray *modelArray = [[NSMutableArray alloc] init];
        for (int i = 0; i < tmpArray.count; i++)
        {
            TableViewModel *model = [[TableViewModel alloc] init];
            model.title = [NSString stringWithFormat:@"第 %@ 个cell", @(i)];
            model.content = tmpArray[i];
            model.imageName = (arc4random() % 10 % (i + 1) == 2 ? @"tianshi.png" : @"futou.png");
            if (i % 3 == 1)
            {
                model.imageName = nil;
            }
            
            [modelArray addObject:model];
        }
        
        _array = [[NSArray alloc] initWithArray:modelArray];
    }
    
    return _array;
}
 
@end

这里需要注意的是,关于iOS11中estimatedRowHeight
简而言之estimatedRowHeight是一个预估高度,iOS11之前是为0,在iOS11下,这个值默认为44。

我们知道tableView是继承于ScrollView的,一个scrollView能滑动,需要设置contentSize,那么tableView的contentSize怎么来呢?iOS11之前,会调用tableView每一个cell的heightForRowAtIndexPath来算出整个高度,从而相加得出contentSize来,这一个步骤挺耗性能!

所以iOS11,默认打开了estimatedRowHeight估算高度功能,当tableView创建完成后,contentSize为estimatedRowHeight(默认值为44)*cell的数量,不需要遍历每一个cell的heightForRowAtIndexPath来计算了。但是这样子真实的contentSize又怎么得出来呢?

不要急,我们看官方文档的描述,里面的一句话

关于iOS11中estimatedRowHeight

根据官方说明可以得出结论,当你的实际高度大于预估高度的时候,会按照预估高度下的cell的数量来计算contentSize,当实际高度小于预估高度的时候,会按照实际高度下的cell的数量来计算contentSize。

如果我们要回到iOS11之前的效果,我们可以让estimatedRowHeight=0,关闭这个预估高度的效果。

为什么使用MJRefresh在iOS11下要让estimatedRowHeight=0,因为MJRefresh底部的上拉刷新是根据contentSize来计算的,当数据更新的时候,得出来的contentSize只是预估的。

上一篇 下一篇

猜你喜欢

热点阅读