转载请注明原作者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
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
具体思路如下图所示:
重用机制代码:
内部查找重用的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方法实现
思路如下图所示;
(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
下载源码查看,不嫌弃的点个星吧:)