iOS-UITableView的使用+原理

一. 关于UITableView

1. UITableViewStyle

作为iOS开发者UITableView可能是最为常用的一个控件,我们都知道在创建UITableView的时候有两种样式可供选择UITableViewStylePlain、UITableViewStyleGrouped。

关于这两种样式到底有什么区别,该如何选择。说实话,在相当长的一段时间里我是稀里糊涂的。下面我来分享一下经验,仅供参考,若有偏差望请指正!

① UITableViewStylePlain

  1. 如果有sectionHeader,区头会出现悬浮吸附的效果。
  2. 如果没有sectionHeader、sectionFooter,cell会铺满整个table。
  3. UITableViewStylePlain的tableView可以有一个section索引,作为一个bar在table的右边(例如A ~ Z)。你可以点击一个特定的标签,跳转到目标section,例如iPhone的通讯录。
UITableViewStylePlain
补充:

UITableViewStylePlain样式可以使用系统的索引条,使用方法如下:

//返回索引的数组
-(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
    //索引文字颜色
    tableView.sectionIndexColor = [UIColor snbcl_colorWithHexString:@"55607C"]; 
    //索引条背景颜色
    tableView.sectionIndexBackgroundColor = [UIColor clearColor]; 
    //返回索引数组
    return [NSArray arrayWithObjects:@"A",@"B",@"C",@"D",@"E",@"F",@"G",@"H",@"I",@"J",@"K",@"L",@"M",@"N",@"O",@"P",@"Q",@"R",@"S",@"T",@"U",@"V",@"W",@"X",@"Y",@"Z",@"#", nil];
}

//点击了哪个索引
-(NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{
    //点击系统的某个索引,就会回调这个方法,title为索引的标题,index为点击了哪个索引
    //我们需要返回一个值,用来告诉系统,tableView滚动到哪个索引
    return  0;
}

② UITableViewStyleGrouped

  1. 如果有sectionHeader、sectionFooter,区头区尾会和cell一样正常滚动,没有悬浮吸附的效果。
  2. 如果没有人为设置sectionHeader、sectionFooter,会有个默认的sectionHeader、sectionFooter效果。
  3. 如果不想要这个默认的sectionHeader、sectionFooter效果,可以设置他们的高度为很小值,不能为0,为0系统会使用默认值,设置为最小值之后,cell会铺满整个table。
  4. UITableViewStyleGrouped的tableView不能有一个(右边的)索引,比如iPhone的设置界面。
UITableViewStyleGrouped

共同点:

两种样式都可以设置tableHeaderView、tableFooterView,并且都没有悬浮吸附的效果。

注意点:

  1. tableview的sectionFooterHeight、sectionFooterHeight属性只在UITableViewStyleGrouped类型,并且未实现代理方法tableView:heightForHeader/FooterInSection: 时有效。
  2. 所以设置sectionHeader、sectionFooter的高度以及自定义sectionHeader、sectionFooter的时候最好使用如下代理方法,这样就不用考虑这么多了。
//设置sectionHeader高度
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0.00001;
}
//设置sectionFooter高度
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    return 0.00001;
}

总结:

  1. 如果有sectionHeader吸附悬浮的需求,或者你不需要sectionHeader、sectionFooter,那么建议使用UITableViewStylePlain。
  2. 如果你需要使用sectionHeader、sectionFooter,并且没吸附效果的要求,那么建议使用UITableViewStyleGrouped。
  3. 很容易理解:Plain样式有悬浮效果,所以可以设置索引条,Grouped样式本来就是分组的意思,所以会有系统自动生成的sectionHeader、sectionFooter

2. UITableViewCellStyle

系统的UITableViewCell有四种样式,如果cell不是太复杂我们可以使用系统的cell,先看代码:

- (UITableView *)tableView {
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0,StatusAndNavBarHight,ScreenWidth,ViewSafeHeight) style:UITableViewStylePlain];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        // 设置cell分隔线
        _tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
        // 默认情况下不显示cell的地方也有分割线,解决办法如下:
        _tableView.tableFooterView = [UIView new];
    }
    return _tableView;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellId = @"cellid";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellId];
    }
    
    cell.imageView.image = [UIImage imageNamed:@"car"];
    cell.textLabel.text = @"我是测试标题";
    cell.detailTextLabel.text = @"我是测试副标题";
    //设置附件样式,一般使用右箭头样式
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    
    // 设置cell选择样式 默认选中灰色
    cell.selectionStyle = UITableViewCellSelectionStyleDefault;
    // 设置分隔线的位置 默认是{0, 15, 0, 0}即左边有15的距离没横线
    // 如果设置为UIEdgeInsetsZero,则整个宽度都有横线
    // cell.separatorInset = UIEdgeInsetsZero;
    return cell;
}

系统的UITableViewCell有四个子控件,分别是imageView、textLabel、detailTextLabel、accessoryType。
对于每一种样式,并不是所有的子控件都可以显示,具体效果如下:

① UITableViewCellStyleDefault

UITableViewCellStyleDefault.png

如果是Default样式,最多只能显示imageView、textLabel、accessoryType这三个控件,如果不设置imageView,则textLabel会往左靠,如果设置accessoryType为UITableViewCellAccessoryNone,则向右的箭头不显示。

② UITableViewCellStyleSubtitle

UITableViewCellStyleSubtitle.png

同理,如果不设置imageView,则右边的控件会左移,如果右边的控件只设置一个那么这个控件会居中显示。

③ UITableViewCellStyleValue1

UITableViewCellStyleValue1.png

detailTextLabel会一直固定在右侧,如果不设置imageView,textLabel会左移。

④ UITableViewCellStyleValue2

UITableViewCellStyleValue2.png

蓝色的是textLabel,黑色的是detailTextLabel,如果只设置一个,另外一个会往左移。

补充

  1. 默认点击cell之后一直有选中效果,如果想点击一下再取消选中效果,可写如下代码:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}
  1. 当我们设置accessoryType = UITableViewCellAccessoryDisclosureIndicator;会显示向右的箭头,当我们设置accessoryView之后,优先显示accessoryView不显示向右的箭头,比如设置accessoryView为UISwitch,如下:

总结:

如果我们不需要detailTextLabel,直接使用默认的样式就好,如果需要detailTextLabel,可以使用Value1样式。

3. UITableView的简单使用

//创建tableView
- (UITableView *)mineTableView
{
    if (!_mineTableView) {
        _mineTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, StatusBarHeight + NavigationBarHeight, ScreenWidth, ViewSafeHeight) style:UITableViewStyleGrouped];
        _mineTableView.showsVerticalScrollIndicator = NO;
        _mineTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
        _mineTableView.delegate = self;
        _mineTableView.dataSource = self;
        _mineTableView.backgroundColor = [UIColor colorWithHex:0xf3f3f3];
        _mineTableView.accessibilityIdentifier = @"XUMineTableView";
        _mineTableView.estimatedRowHeight = 0;
        _mineTableView.estimatedSectionFooterHeight = 0;
        _mineTableView.estimatedSectionHeaderHeight = 0;
        if (@available(iOS 11.0, *)) {
            [_mineTableView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
        } else {
            self.automaticallyAdjustsScrollViewInsets = NO;
        }
    }
    return _mineTableView;
}

//获取cell
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *rid = @"XUMineCellIdentify";
//如果在创建tableView的时候注册cell了,在使用dequeueReusableCellWithIdentifier获取cell的时候就不用判空了,因为注册后一定可以获取得到。
    XUMineCell *cell = [tableView dequeueReusableCellWithIdentifier:rid];
//如果在创建tableView的时候没有注册cell,在获取cell的时候就需要判空。
    if(cell == nil){
       cell = [[XUMineCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:rid];
    }
}

补充:关于tableView的tableHeaderView和tableFooterView

  1. 默认情况下,他们的位置布局如下:
tableHeaderView
sectionHeaderView
cell
... 其他cell
sectionFooterView
... 其他组
tableFooterrView

如果tableView有内边距,比如设置上内边距为100:UIEdgeInsets(top: 100, left: 0, bottom: 0, right: 0),那么它们都会被挤下去,如下图,蓝色背景的是tableHeaderView,下面分别是sectionHeaderView、cell,蓝色背景上面的就是100的内边距。

  1. 如果是通过frame的方式设置tableHeaderView、tableFooterView,那么tableHeaderView一直在最上方,tableFooterView一直在最下方,通过frame只能修改它们的高度。
  2. 如果是通过snapkit或者masonry就可以任意修改它们的位置和宽高(不知道为什么)。
  3. reloadData的时候会刷新tableHeaderView、tableFooterView,比如如果修改了tableHeaderView的高度,就需要reloadData后才会生效。

总结:使用tableHeaderView、tableFooterView,我们就用frame进行布局

二. UITableView的重用机制

系统的UITableView是继承于UIScrollView,所以可以滑动,关于UITableView最主要的就是重用机制,下面验证UITableView的重用机制,其中_tableview是通过xib拖过来的,代码如下:

@interface ViewController (){
    
    IBOutlet UITableView *_tableview;
   //一共使用了多少个cell(重用池+现有池个数)
    NSMutableArray *_cellAry;  
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationController.navigationBar.translucent = NO;

    _cellAry = [NSMutableArray new];
    _tableview.estimatedRowHeight = 0; // 预估高度,默认是44, ios11
    [_tableview registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
}

#pragma mark - tableView delegate
// 646  一屏幕最多展示多少个cell:5
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 30;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"%ld", (long)indexPath.row);
    return 200;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    BOOL isContain = NO;
    for(NSValue *value in _cellAry){
        if ([value.nonretainedObjectValue isEqual:cell]) {
            isContain = YES;
            break;
        }
    }
    if (!isContain) {
        //弱引用
        //详情可参考:https://www.jianshu.com/p/51156d4ae885
        NSValue *value = [NSValue valueWithNonretainedObject:cell];
        [_cellAry addObject:value];
    }
    cell.textLabel.text = [@(indexPath.row) description];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    // 验证预估高度
    // NSLog(@"%f", _tableview.contentSize.height);
    NSLog(@"==%lu", (unsigned long)_cellAry.count);
}

当我们拖动tableView并且点击其中一个Cell, 会打印如下:

2019-10-22 14:10:20.631702+0800 TableviewAnly[11882:1011989] 11
2019-10-22 14:10:20.632040+0800 TableviewAnly[11882:1011989] 11
2019-10-22 14:10:21.415626+0800 TableviewAnly[11882:1011989] 12
2019-10-22 14:10:21.416440+0800 TableviewAnly[11882:1011989] 12
2019-10-22 14:14:49.007275+0800 TableviewAnly[11882:1011989] ==5

可以发现数组中一共有5个cell, 但是如下图, 最多只能展示4个Cell,另外1个Cell去哪了呢?

重用池.png

简析:

  1. 重用机制,会一直保持cell不变的数量(5)
  2. 问题:当界面显示4个时候, 另外1个去哪里了?
    重用池(没有显示在界面上的cell: 1个)+ 现有池(visible即显示在界面上的cell: 4个)= 5个。
  3. 问题:是如何控制显示哪些的呢? 哪些显示在界面上,哪些不显示呢?
    table在加载的时候,每个cell的位置信息被保存起来了,数据先行,UI后走(位置信息被保存下来了,那么哪些显示哪些不显示自然就知道了)。

总结:

通过上面的代码我们就知道现有池有4个cell,重用池有1个cell,所以一共5个cell,而且滚动的时候一直是5个cell,从而就验证系统UITableView的重用机制。

三. 模仿系统的UITableView

因为系统的tableView在加载的时候,每个cell的位置信息被保存起来了,所以我们先创建个模型,代码如下:

#import 
#import 
@interface EOCCellModel : NSObject

@property (nonatomic, assign)CGFloat y;
@property (nonatomic, assign)CGFloat height;

@end

自定义一个继承于UIScrollView的EOCTableView,代码如下:
EOCTableView.h文件

#import 

@class EOCTableView;

//数据源代理
@protocol EOCTableViewDelegate

@required
- (NSInteger)tableView:(EOCTableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (CGFloat)tableView:(EOCTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (UITableViewCell *)tableView:(EOCTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@end

@interface EOCTableView : UIScrollView

@property (nonatomic,weak)iddelegate; //数据源代理

//刷新cell方法
- (void)reloadData; 
//重用方法
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;

@end

EOCTableView.m文件

#import "EOCTableView.h"
#import "EOCCellModel.h"

@interface EOCTableView(){
    NSMutableDictionary *_visibleCellDict; // 现有池
    NSMutableArray *_reusePoolCellAry; // 重用池
    NSMutableArray *_cellInfoArr; // cell 信息 (y值,高度,数量信息)
}

@end

// 667  cell高度 60,    660/60 = 11,7个像素可以显示2个残的  界面最多可以显示 11 + 2 = 13

@implementation EOCTableView

/*
 1现有池
 2重用池
 3位置信息
 */
- (instancetype)initWithFrame:(CGRect)frame{
    
    self = [super initWithFrame:frame];
    if (self) {
        _visibleCellDict = [NSMutableDictionary new];
        _reusePoolCellAry = [NSMutableArray new];
        _cellInfoArr = [NSMutableArray new];
    }
    return self;
}

#pragma mark - 数据,UI
/*
 数据, UI
 */
- (void)reloadData{
    
    // 1 处理数据
    [self dataHandle];
    // 2 UI 处理
    [self setNeedsLayout];
    
}

// 1. 处理数据, 数据model不会复用,只是保存下来
- (void)dataHandle{
    
    // 1.1 获取cell的数量
    NSInteger allCellCount = [self.delegate tableView:self numberOfRowsInSection:0];
    
    [_cellInfoArr removeAllObjects]; // 移除旧的信息
    
    CGFloat totalCellHeight = 0;
    for (int i = 0; i < allCellCount; i++) {
        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        CGFloat cellHeight = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
        //1.2 获取cell的高度和y值, 并保存起来
        EOCCellModel *model = [EOCCellModel new];
        model.y = totalCellHeight;
        model.height = cellHeight;
        
        [_cellInfoArr addObject:model];
        
        totalCellHeight += cellHeight;
    }
    
    //根据总高度设置可以滑动的范围
    [self setContentSize:CGSizeMake(self.frame.size.width, totalCellHeight)];
}

// 2. UI处理, UI会复用
- (void)layoutSubviews{
    
    [super layoutSubviews];
    
    // 2.1 计算可视范围,要显示哪些cell,并把相关cell显示到界面
    CGFloat startY = self.contentOffset.y;
    CGFloat endY = self.contentOffset.y + self.frame.size.height;
    if (startY < 0) {
        startY = 0;
    }
    if (endY > self.contentSize.height) {
        endY = self.contentSize.height;
    }
    
    // 2.2 计算边界的cell索引 (从哪几个到哪几个cell, 如第3个到第9个)
    
    EOCCellModel *startModel = [EOCCellModel new];
    startModel.y = startY;
    
    EOCCellModel *endModel = [EOCCellModel new];
    endModel.y = endY;
    
    // 2.3 目地就是获取可视区域显示cell的索引范围
    //使用二分查找,替换下面的方法,效率更高
    //查找:用二分查找(1024 = 2的10次方 查找次数最多10次)
    NSInteger startIndex = [self binarySerchOC:_cellInfoArr target:startModel];
    NSInteger endIndex = [self binarySerchOC:_cellInfoArr target:endModel];
    
//    // 2.3.1 开始索引
//    for(NSInteger i = 0; i < _cellInfoArr.count; i++){
//
//        EOCCellModel *cellModel = _cellInfoArr[i];
//
//        if (cellModel.y <= startY && cellModel.y + cellModel.height > startY) {
//            startIndex = I;
//            break;
//        }
//    }
//    // 2.3.2 结束索引
//    for (NSInteger i = startIndex + 1; i < _cellInfoArr.count; i++) {
//
//        EOCCellModel *cellModel = _cellInfoArr[i];
//        if (cellModel.y < endY && cellModel.y + cellModel.height >= endY) {
//            endIndex = I;
//            break;
//        }
//    }
    
    // 2.4 UI操作 获取cell,并显示到View上
    for (NSInteger i = startIndex; i <= endIndex; i++) {
        
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        //这个代理方法里面执行了重用机制方法dequeueReusableCellWithIdentifier
        UITableViewCell *cell = [self.delegate tableView:self cellForRowAtIndexPath:indexPath];
        // 当row = 3 的时候, 如果现有池有个相同的CellA,就返回cellA
        // 当row = 4 的时候,现有池不存相关的cell,然后去重用池读取cell,发现重用池有一个cellA,于是就返回了cellA
        
        EOCCellModel *cellModel = _cellInfoArr[i];
        cell.frame = CGRectMake(0, cellModel.y, self.frame.size.width, cellModel.height);
        
        if (![cell superview]) {
            [self addSubview:cell]; // 添加到tableview上  // addsubivew
        }
    }
    
    // 2.5 从现有池里面移走不在界面上的cell,移动到重用池里(把不在可视区域的cell移到重用池)
    NSArray *visibelCellKey = _visibleCellDict.allKeys;
    for (NSInteger i = 0; i < visibelCellKey.count; i++) {
        
        NSInteger index = [visibelCellKey[i] integerValue];
        if (index < startIndex || index > endIndex) {
            
            [_reusePoolCellAry addObject:_visibleCellDict[visibelCellKey[i]]];
            [_visibleCellDict removeObjectForKey:visibelCellKey[I]];
        }
    }
}

#pragma mark - 重用的根本方法  先从现有池拿,没有再从重用池拿,没有再创建
// 重用池Model/UI 和 现有池Model/UI
// 重用池和现有池有同一个 cellA,现有池的cellA 就会返回出来
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath{
    
    // 1. 是否在现有池里面,如果有,从现有池里面读取对应的cell返回
    UITableViewCell *cell = _visibleCellDict[@(indexPath.row)];
    if(!cell){
        // 2. 现有池如没有,再看重用池
        // 2.1 重用池是否有没有用的cell,有就返回cell
        if(_reusePoolCellAry.count > 0){
            cell = _reusePoolCellAry.firstObject;
        
        }else{
        // 2.2 重用池没有就创建
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
        }
        // 2.3 保存到现有池,移除重用池
        [_visibleCellDict setObject:cell forKey:@(indexPath.row)];// 保存到现有池
        [_reusePoolCellAry removeObject:cell]; // 移除重用池
    }
    
    return cell;
}

#pragma mark - 二分查找查找索引操作
- (NSInteger)binarySerchOC:(NSArray*)dataAry target:(EOCCellModel*)targetModel{
    
    NSInteger min = 0;
    NSInteger max = dataAry.count - 1;
    NSInteger mid;
    while (min < max) {
        mid = min + (max - min)/2;
        // 条件判断
        EOCCellModel *midModel = dataAry[mid];
        if (midModel.y < targetModel.y && midModel.y + midModel.height > targetModel.y) {
            return mid;
        }else if(targetModel.y < midModel.y){
            max = mid;// 在左边
            if (max - min == 1) {
                return min;
            }
        }else {
            min = mid;// 在右边
            if (max - min == 1) {
                return max;
            }
        }
    }
    return -1;
}

@end

在SecondViewCtr添加如下代码:

#import "SecondViewCtr.h"
#import "EOCTableView.h"

@interface SecondViewCtr (){
    EOCTableView *_tableView;
}
@end

@implementation SecondViewCtr

- (void)viewDidLoad {
    [super viewDidLoad];
    _tableView = [[EOCTableView alloc] initWithFrame:self.view.frame];
    _tableView.delegate = self;
    [_tableView reloadData];
    [self.view addSubview:_tableView];
}

- (NSInteger)tableView:(EOCTableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 200;
}

//性能优化1: 假如高度都不一样 【tableView reload】去计算 重新计算200个cell的高度,计算量会很大,所以常见的优化手段就是保存高度
- (CGFloat)tableView:(EOCTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return  60;
}

- (UITableViewCell *)tableView:(EOCTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//性能优化2:UI,cell的处理更少占用主线程(使用SDWebImage异步加载,缓存)
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
     cell.textLabel.text = [@(indexPath.row) description];
    return cell;
}
@end

运行后,效果图如下:
自定义tableView.png

滑动后,在EOCTableView的layoutSubviews方法打个断点,po一下:

(lldb) po _visibleCellDict.allKeys.count
12

(lldb) po _reusePoolCellAry.count
2

发现现有池12个,重用池2个,实现了模仿系统的tableView。

总结:

系统tableView重用Cell的根本方法就是dequeueReusableCellWithIdentifier方法,这个方法是在系统调用cellForRowAtIndexPath方法的内部调用的,dequeueReusableCellWithIdentifier内部做的事是:先从现有池拿,没有再从重用池拿,没有再创建

关于代码的讲解就省略了,看注释吧。

四. 系统UITableView的优化

最基本的两个方法:

  1. 缓存Cell高度
    假如高度都不一样 [tableView reload] 去计算,重新计算200个cell的高度,计算量会很大,所以常见的优化手段就是保存高度。
  2. 异步加载Cell图片
    Cell的处理更少占用主线程(使用SDWebImage异步加载、缓存)

代码如上:

五. 实现UITaleView悬浮两个头视图的效果

自定义taleView,代码如下:

#import 
@interface TaskTableView : UITableView

@property (nonatomic, weak)UIView *secionOneHeadView;
@property (nonatomic, weak)UIView *secionTwoHeadView;

@end
#import "TaskTableView.h"

@implementation TaskTableView

- (void)layoutSubviews{
    [super layoutSubviews];
    // 重新布局headView位置
    CGFloat headViewWidth = self.secionOneHeadView.frame.size.width;
    CGFloat headViewHeight = self.secionOneHeadView.frame.size.height;
    //当前面五个cell已经滑过去的时候,重新布局
    if (self.contentOffset.y > 70 * 5 ) {
        //重新设偏移量
        self.secionOneHeadView.frame = CGRectMake(0, self.contentOffset.y, headViewWidth, headViewHeight);
        self.secionTwoHeadView.frame = CGRectMake(0, self.contentOffset.y + headViewHeight, headViewWidth, headViewHeight);
    }
    //系统默认会移除,所以我们重新添加上去
    if (![self.secionOneHeadView superview]) {
        [self addSubview:self.secionOneHeadView];
    }
}
@end

在ThridViewCtr.xib内容如下图:
xib.png

在ThridViewCtr.m实现如下代码:

#import "ThridViewCtr.h"
#import "TaskTableView.h"
@interface ThridViewCtr (){

    IBOutlet TaskTableView *_tableview;
    IBOutlet UIView *_sectionOneHeadView;
    IBOutlet UIView *_sectionTwoHeadView;
}
@end

@implementation ThridViewCtr

- (void)viewDidLoad {
    [super viewDidLoad];
 
    [_sectionOneHeadView removeFromSuperview];
    [_sectionTwoHeadView removeFromSuperview];
    
    _tableview.secionOneHeadView  = _sectionOneHeadView;
    _tableview.secionTwoHeadView  = _sectionTwoHeadView;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    if (section == 0) {
        return 5;
    }
    return 20;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 70;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    return 80;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    if (section == 0) {
        return _sectionOneHeadView;
    }else{
        return _sectionTwoHeadView;
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"%d--%d", indexPath.section, indexPath.row];
    return cell;
}
@end

运行后向上拖动tableView发现, 两个headerView会一直停留在顶部, 效果图如下:
两个头视图悬浮.png

总结:

实现方法就是拿到头视图引用,在tableView的layoutSubviews方法里面修改头视图偏移量,再重新添加到tableView上。

六. 其他注意点

  1. 调用reloadData方法并不是立马执行,是异步的,会在下一个RunLoop循环里面执行。
  2. tableview.estimatedRowHeight = 0;
    ① tableView的预估高度,iOS11之后出现的,在一个cell还没有显示到界面上,系统会给一个预估高度,默认44。
    ② 如果这个属性不设置为0,在这个时候去获取tableview.contentSize.height是不准的。
    ③ 这个在MJ_footer里面,如果不设置预估高度为0,拿到的contentSize不准,会出现bug。

Demo地址:https://github.com/iamkata/tableView

你可能感兴趣的:(iOS-UITableView的使用+原理)