【原】通过实现一个横向Tableview,了解UITableview工作原理

转载请注明原作者coderZ

UITableview代理方法介绍

UITableview有两个相关代理UITableViewDelegate、UITableViewDataSource
dataSource是数据源代理,delegate则是相关操作代理

dataSource

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

通过返回值,告诉tableview的某个section应该显示多少个单元格
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
通过返回值,告诉tableview,indexPath索引下的单元格的高度,tableview单元格的宽度与tableview相同
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
通过返回cell,告诉tableview,indexPath索引下应该展现的单元格
以上便是tableview dataSource最基本的,也是必须实现的三个代理,通过这三个代理可以展现一个最基本的tableview

delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

点击单元格回调方法

UITableViewCell以及重用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell == nil) {
        UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    return cell;
}

以上是tableview实现重用的基本写法,通过创建一个带有重用标识符的cell,tableview每次通过代理获取cell时,都会先从重用池中获取,节省内存消耗。
以上是tableview最基础的几个代理方法,下面通过代码实现这几个代理方法,一窥tableview内在工作的原理

实现一个横向的tableview:MinScrollMenu

ps:源代码已上传至github:MinScrollMenu

【原】通过实现一个横向Tableview,了解UITableview工作原理_第1张图片
introduce.gif
1 定义代理
- (NSInteger)numberOfMenuCount:(MinScrollMenu *)menu;
- (CGFloat)scrollMenu:(MinScrollMenu*)menu widthForItemAtIndex:(NSInteger)index;
- (MinScrollMenuItem *)scrollMenu:(MinScrollMenu*)menu itemAtIndex:(NSInteger)index;
- (void)scrollMenu:(MinScrollMenu*)menu didSelectedItem: (MinScrollMenuItem *)item atIndex: (NSInteger)index;

模仿之前介绍的四个代理方法。

2 布局

创建一个继承UIView的子类,命名为MinScrollMenu。
(1)添加一个scrollView属性,初始化加到MinScrollMenu上,frame大小和父视图一样。正如系统的UITableView一样,我们也使用scrollView来实现功能

@property (nonatomic, strong) UIScrollView *scrollView;/*!< 横向滚动的scrollView */

(2)添加一个继承自UIView的属性,命名为contentView,初始化加到之前创建好的scrollView上。frame可以先不设置,这个view主要用来装载将来要显示的单元格,frame大小需要以后计算。

@property (nonatomic, strong) UIView *contentView;/*!< 装载item的view */

(3)以下几个属性主要用来缓存单元格数据源的数据

@property (nonatomic, strong) NSMutableArray *visibleItems;/*!< 屏幕范围内的item数组 */
@property (nonatomic, strong) NSMutableSet *reuseableItems;/*!< 重用池 */
@property (nonatomic, strong) NSMutableDictionary *infoDict;/*!< 缓存item被选中信息 */
@property (nonatomic, strong) NSMutableDictionary *frameDict;/*!< 缓存item的frame */
3 处理数据源数据

(1) 根据代理获取item个数

    if (self.delegate != nil && [self.delegate respondsToSelector:@selector(numberOfMenuCount:)]) {
        _count = [self.delegate numberOfMenuCount:self];
    }

(2) 循环创建单元格item,因为是横向滚动的,所以主要获取宽度和改变x轴的值计算frame。计算出所有的item的frame并装在字典中缓存,然后就可以得出之前没有设置的contentView的frame了,贴出代码:

    for (NSInteger i = 0; i < _count; ++i) {
        //获取item的宽度
        width = [self itemWidthWithIndex:i];
        CGRect itemFrame = CGRectMake(x, y, width, height);
        
        // 超过屏幕可显示范围不加入到visibleItems数组
        CGFloat maxX = CGRectGetMaxX(itemFrame);
        CGFloat overItemWidth = width*3;
        if (i < _count-3) {
            overItemWidth = width + [self itemWidthWithIndex:i+1] + [self itemWidthWithIndex:i+2];
        }
        isOverScreenWidth = maxX > ScreenWidth + overItemWidth;
        if (!isOverScreenWidth) {
            // 获取item,设置Frame, 添加到contentView上
            MinScrollMenuItem *item = [self itemWithIndex:i];
            if (item) {
                item.frame = itemFrame;
                [_contentView addSubview:item];
                
                // 添加点击手势
                UITapGestureRecognizer *tapGst = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapItem:)];
                [item addGestureRecognizer:tapGst];
                
                item.tag = ITEMTAG + i;
                
                // 加入到visibleItems数组
                [_visibleItems addObject:item];
            }
        }
        
        // 缓存数据
        [_frameDict setObject:@(i) forKey:NSStringFromCGRect(itemFrame)];
        [_infoDict setObject:@(NO) forKey:@(i)];
        
        // 计算scrollView的contentSize
        scrollContentWidth = maxX;
        
        x += width;
    }
    
    _scrollView.contentSize = CGSizeMake(scrollContentWidth, height);
    _contentView.frame = CGRectMake(0, 0, scrollContentWidth, height);

完成以上代码,运行一下。就可以看见item显示了,但是滚动处理还没有完成,所以手指拖动scrollView右边区域还是空白一片,接下来就是核心的滚动处理和重用机制的实现
(3) 重用和滚动处理
重用和滚动处理是同时进行的,当tableView向右滚动时,如果最左边的item已经离开屏幕范围,那么就可以将它放进重用池中存储,同时也要根据item的标识符从重用池里取出item,设置frame,添加到visibleItems数组。如此就可以循环使用几个item来展现n个item的内容了。
实现UIScrollView代理方法
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
通过这个方法可以获取当前scrollView移动的位移contentOffset
具体思路如下图所示:

【原】通过实现一个横向Tableview,了解UITableview工作原理_第2张图片
滚动.png

重用机制代码:
内部查找重用的item:

    NSSet *tempSet = [_reuseableItems filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"reuseIdentifer == %@", reuseItem.reuseIdentifer]];
    // 查询复用池中有没有相同复用符的item
    if (![tempSet isSubsetOfSet:_reuseableItems] || tempSet.count == 0) {
        // 没有则添加item到复用池中
        [_reuseableItems addObject:reuseItem];
    }

公开API实现的代码:

- (MinScrollMenuItem *)dequeueItemWithIdentifer:(NSString *)identifer {
    NSSet *tempSet = [_reuseableItems filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"reuseIdentifer == %@", identifer]];
    MinScrollMenuItem *item = tempSet.anyObject;
    return item;
}

也可以不用谓词查询,直接使用循环查找,因为重用池每种标识符item一般只需要一个就足够了。所以Set元素个数比较少。
(4)数据刷新reloadData方法实现
思路如下图所示;

【原】通过实现一个横向Tableview,了解UITableview工作原理_第3张图片
数据刷新.png

(5)点击item回调响应方法实现:
Menu内的实现:
首先,将遍历之前保存选中状态的字典,如果value是YES,则修改为NO
第二,遍历屏幕显示item数组visibleItems,将item的isSelected属性设为NO
第三,将选中的item状态改为选中,通过tag值获取index索引,保存到缓存字典中。
第四,回调代理方法,通知控制器
贴上具体代码:
- (void)tapItem: (UITapGestureRecognizer *)tapGst {
[_infoDict enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL * _Nonnull stop) {
stop = obj.boolValue;
if (
stop) {
_infoDict[key] = @(NO);
}
}];
for (MinScrollMenuItem *item in _visibleItems) {
item.isSelected = NO;
[_infoDict setObject:@(NO) forKey:@(item.tag-ITEMTAG)];
}

    if ([tapGst.view isKindOfClass:[UIView class]]) {
        
        UIView *tempView = tapGst.view;
        MinScrollMenuItem *item = (MinScrollMenuItem *)tempView;
        
        if ([item isKindOfClass:[MinScrollMenuItem class]]) {
            item.isSelected = YES;
            [_infoDict setObject:@(YES) forKey:@(item.tag-ITEMTAG)];
            if (self.delegate && [self.delegate respondsToSelector:@selector(scrollMenu:didSelectedItem:atIndex:)]) {
                [self.delegate scrollMenu:self didSelectedItem:item atIndex:item.tag - ITEMTAG];
            }
        }
    }
}

Item内的实现:
首先,item添加一个选中状态的CALayer类作为属性,创建好添加到item的layer上,不要忘记了设置为隐藏,hidden=YES。再提供一个对外开放的BOOL值isSelected属性。
第二,重写isSelected属性set方法,被选中时修改layer的hidden为NO即可。

后记:tableview的四个基本代理方法已经都实现了。通过这个横向滚动的类tableview控件,对tableview的工作原理有了更深一层的认识,当然tableview还有很多功能没有实现,但是基本框架完成了,一些功能性的东西后面陆续可以添加。大家可以通过github地址:MinScrollMenu
下载源码查看,不嫌弃的点个星吧:)

你可能感兴趣的:(【原】通过实现一个横向Tableview,了解UITableview工作原理)