iOS:UITableView相关(20-12-31更)
UITableView用得较多,遇到的情况也较多,单独记录一篇。
一、零散的技巧
二、取cell
三、cell高度
四、导航栏、TableView常见问题相关
五、自定义左滑删除按钮图片
六、自定义长按手势拖动
七、仅做了解
一、零散的技巧
1、 cell的选中效果是cell的属性,可以有的有,无的无。
// 自定义cell self.selectionStyle = UITableViewCellSelectionStyleNone; // 取cell cell.selectionStyle = UITableViewCellSelectionStyleNone;
2、cell的下划线是Table的属性,全部有,或全部无。
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
3、cell下划线左边顶住屏幕左边。
cell.preservesSuperviewLayoutMargins = NO; cell.layoutMargins = UIEdgeInsetsZero; cell.separatorInset = UIEdgeInsetsZero;
后续补充:也可以隐藏掉系统的下划线,自定义LineView,要多宽就多宽,且可以实现不同cell不同下划线样式。
4、cell的重用ID,可以用类名
NSStringFromClass([MyCell class])
5、根据 indexPath 获取 cell
[self.mTableView cellForRowAtIndexPath:indexPath];
6、根据 cell 获取 indexPath
[self.mTableView indexPathForCell:cell];
7、superView
// 第一个superview 是contentView,第二个就cell UITableViewCell *cell = (UITableViewCell*)button.superview.superview;
8、UIScrollView 和 UITableView 的 内边距差别
// 自动偏移 contentOffset = CGPointMake(0, -200); tableView.contentInset = UIEdgeInsetsMake(200, 0, 200, 0); // 需要设置偏移量。否则停留在偏移量(0.0)。需要再下拉一下, scrollView.contentInset = UIEdgeInsetsMake(200, 0, 200, 0); scrollView.contentOffset = CGPointMake(0, -200);
9、监听 contentOffset ,可以得到类似拖动代理的效果。如写第三方给别人用。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ }
10、取消cell的左滑 编辑、删除 状态。如,按了其他位置的按钮,cell不会自动复原。
[self.mTableView setEditing:NO animated:YES];
11、style ,风格样式
// 分组 风格 // 1、自动间隔开每组 // 2、如需设置间隔需要注意,组头组尾都要处理。(坑过一次,只设置组尾高度,结果发现怎么还很高,而且不显示不能为0,要 = CGFLOAT_MIN) // 3、滑动,组头不会悬停 self.mTableView = [[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStyleGrouped]; // 扁平化 风格 // 1、每组的间隙可通过组头、组尾,自行调整。(相对上面风格,组头组尾高度默认为0) // 2、滑动,组头组尾会悬停 self.mTableView = [[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStylePlain];
12、获取当前显示的cells
// 直接得到 cells self.mTableView.visibleCells // 得到 indexPath ,看需求通过 cellForRowAtIndexPath: 转换。 self.mTableView.indexPathsForVisibleRows
13、滑动时,使用低分辨率图片,停止时再加载高分辨率图片。(利用 代理 和上面 “12、获取当前显示的cells” )
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
14、UIScrollView、UITableView 实时 位置 相关
参照 《iOS:手势与矩形、点运算相关》 -> “1、矩形、点运算” -> “4、UIScrollView、UITableView 实时 位置 相关”
15、拖动状态。比如判断当前滚动是否拖动引起。拖动的代理只有开始和结束,拖动中没有。
mScrollView.dragging
16、滚动到具体的cell位置。如,1、外卖,左右tableView联动。2、聊天,滚动到最新信息。
if (self.dataSource.count > 0) { [self.mTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.dataSource.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; }
17、选中效果,动画
// 用途可参考外卖、电商类APP(左边tbV的分类,随着右边商品的拖动,跟新cell选中位置) [self.leftTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:section inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
N、如果一个 tableView 对应多个 dataSource 。通过按钮切换,那么要考虑,点击/滑动 切换时,请求返回的数据,是否是当前 “功能选中”的位置,比如:
判断对比请求前后的字段 parameterDics、状态。若不是,
1)、可丢弃。
2)、可刷新该状态对应的 dataSource 数组(有的话),下次切换,可先刷出数据,再请求。界面友好(防止网络请求,一片空)。
边输入边搜索,同理,避免,如快速删除完后,又刷出删除前的请求数据。
二、取cell
1、cell初始化的一些区别
1)、TableViewCell
1-1)、没注册
没注册的(一开始会取不到): cell = 从队列取 if(cell取不到) { 创建cell 创建子视图,加tag } cell从tag取子视图,刷新 tag 或 属性
1-2)、注册
注册的(100%取得到): cell = 从队列取(有indexPath的方法) 刷新 tag 或 属性 ( 系统取不到,会走自定义的initWithStyle:reuseIdentifier: if(cell创建成功) { 创建子视图,加tag } )
2)、CollectionViewCell
2-1)、没注册
2-2)、注册
注册的(100%取得到): cell = 从队列取(有indexPath的方法) if(cell取得到) { (判断是否有子视图)创建子视图 } 刷新 tag 或 属性 collectionViewCell 流程有点不同 1、没 TableViewCell 的 initWithStyle:reuseIdentifier: 2、但 每次都能从队列取到 3、所以 需要判断取到的cell是否有子视图,不然会不断创建
2、加载XIB
1)、从多个cell样式的XIB加载。只有1个cell样式,可直接lastObject加载。(先根据不同的ID取,取不到再加载。)
1-1)、获取XIB里的所有对象
NSArray *cellArry = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([MyTableCell class]) owner:self options:nil];
1-2)、读取对应的Cell样式,此时的参数type为枚举,或基本数据类型。
cell = [cellArry objectAtIndex:type];
2)、在 UIView + xxx 的类别文件里,可以添加这个类。方便加载单种Cell样式的XIB。
+ (instancetype)viewFromXib { return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self) owner:nil options:nil] lastObject]; }
三、cell高度
0、不固定内容的cell,可弄数组、模型存高度,以免每次计算。
貌似系统计算的(下面的4、5、),耗时都长?复杂的cell滑动不流畅?所以还是能手动就手动咯(下面的2、3、)?
还有,如果是富文本,记得要把font加进去计算,经常算行距的时候,忘了字体大小。
1、全部固定高度
self.tableView.rowHeight = 44;
2、自定义cell类方法
+ (CGFloat)getCellHeight { return 44; } + (CGFloat)getCellHeightWithData:(id)data { // 手动计算label的高度 return 计算高度; }
后续补充:对于固定高度,没问题,好用。对于根据Data计算的,根据情况保存计算高度。
3、模型(只有属性的特殊类)
后续补充:通过get方法读取。需要才计算(懒加载),可能还要判断是否计算过,否则每次都要计算?
4、系统自动计算(iOS6后,使用 UIView 的 类别 UIConstraintBasedLayoutFittingSize 的方法,控件需要全是 Autolayout 约束?)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // 取出不带 indexPath 的 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class])]; // 填充数据 //cell.model = model[indexPath.row]; [cell initData:data[indexPath.row]]; // 计算高度 // UILayoutFittingCompressedSize 返回最小可能的值 // UILayoutFittingExpandedSize 返回最大可能的值 cellHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 0.5f; return cellHeight; }
后续补充:1、根据情况保存计算高度。
2、普通View非Cell,的高度计算也可用,但同样要 Autolayout 约束。
3、注册cell,一般是取出带indexPath的。不带indexPath一般是在自写cell重用机制的用的。
但是,注册cell 还可以取出普通的cell样式,不带 indexPath。来填充数据,计算高度。
4、对3、补充,如果是xib可以用 NSBundle。
5、对3、再补充,可以弄个局部变量,用懒加载获取普通cell,不用每次都获取。
6、label类,多行,除了 label.numberOfLines = 0。
好像还需要设置 label.preferredMaxLayoutWidth = SCREEN_WIDTH - 20 ;
5、系统自动计算(iOS8后,UITableViewAutomaticDimension,控件需要全是 Autolayout 约束?)
1)、先给cell高度一个估算值,好让TableView,知道contentSize有多大
tableView.estimatedRowHeight = 80.0f;
2)、设置为自动计算
tableView.rowHeight = UITableViewAutomaticDimension;
iOS8后,UITableViewAutomaticDimension自动计算,不用实现 heightForRowAtIndexPath 了,不过为了兼容ios8前,可能需要再写、判断
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if ( [[[UIDevice currentDevice] systemVersion ] integerValue] >= 8) { return UITableViewAutomaticDimension; } else { } }
四、导航栏、TableView常见问题相关
1、导航栏、TableView
//调整contentInset。
//NO:不调整,按设定的frame、contentInset的显示
//YES:会调整contentInset.top的高,让显示的顶在导航栏下面,【有滑过半透明效果】
self.automaticallyAdjustsScrollViewInsets =NO;
//调整frame
// UIRectEdgeNone //会顶在导航栏下面【没有滑过半透明效果】
// UIRectEdgeTop //对齐原点
// UIRectEdgeLeft //对齐左边
// UIRectEdgeBottom //对齐顶部
// UIRectEdgeRight //对齐右边
// UIRectEdgeAll //对齐所有
self.edgesForExtendedLayout = UIRectEdgeNone;
//导航栏半透明
self.navigationController.navigationBar.translucent = YES;
//隐藏navigationBar(1、它推过的所有的VC共用1个Bar;2、用继承View的hidden属性,隐藏不了!)
self.navigationController.navigationBarHidden=YES;
2、iOS11
此处参考自简书 “iOS 11 安全区域适配总结 ” -- sonialiu
1)、TableView 默认开启Cell高度估算,关掉。
[UITableView appearance].estimatedRowHeight = 0; [UITableView appearance].estimatedSectionHeaderHeight = 0; [UITableView appearance].estimatedSectionFooterHeight = 0;
2)、ScrollView新增安全区域。
2-1)、如果之前让TabelView顶住屏幕,然后设置顶部内边距 = 20+44,刚好在导航栏下面的话,
会被系统向下偏移64的 SafeAreaInsets,再加上自己设置的64,就出现下移64问题。
2-2)、同理,没导航栏的时候,也会下移20 -> 状态栏的高度。
2-3)、以前若设置 automaticallyAdjustsScrollViewInsets = YES 让系统自动调整,不会有问题
解决方案:添加下面,相当于 automaticallyAdjustsScrollViewInsets = NO
#ifdef __IPHONE_11_0 if ([tableView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { [UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } #endif
2-4)、contentInsetAdjustmentBehavior 其他类型
UIScrollViewContentInsetAdjustmentScrollableAxes: adjustedContentInset = ( 可滚动方向 ? safeAreaInset + contentInset : contentInset );
UIScrollViewContentInsetAdjustmentNever: adjustedContentInset = contentInset;
UIScrollViewContentInsetAdjustmentAlways: adjustedContentInset = safeAreaInset + contentInset;
UIScrollViewContentInsetAdjustmentAutomatic: (controller里automaticallyAdjustsScrollViewInsets = YES) && (controller被navigation包含) == Always,否则 == Axes
五、自定义左滑删除按钮图片
参考自简书 《【支持iOS11】UITableView左滑删除自定义 - 实现多选项并使用自定义图片 》 -- pika11
0、写在前面
尽管iOS11已经支持自定义删除图片了,但还是要兼容以前的。
1、进入编辑模式,标记view为需要layout。
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = indexPath; [vc.view setNeedsLayout]; } - (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = nil; }
2、在VC的,layout子View完成的时候,判断是否需要改变cell删除样式
-(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (self.dataSource.editingIndexPath != nil) { // 改变cell 删除文字 为 删除图片 [self resetCellDeleteButton]; } }
3、配置,不同系统
- (void)resetCellDeleteButton { // 获取选项按钮的reference if ( [[[UIDevice currentDevice] systemVersion] integerValue] >= 11 ) { for (UIView *subview in self.mTableView.subviews) { //iOS11(Xcode 8编译): UITableView -> UITableViewWrapperView -> UISwipeActionPullView if ([subview isKindOfClass:NSClassFromString(@"UITableViewWrapperView")]) { for (UIView *subsubview in subview.subviews) { if ([subsubview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subsubview.subviews count] >= 1) { #warning - 可能需要判断类型再去改,会比较好,暂时没去试。 UIButton *deleteButton = subsubview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } //iOS11(Xcode 9编译): UITableView -> UISwipeActionPullView else if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subview.subviews count] >= 1) { #warning - 可能需要判断类型再去改,会比较好,暂时没去试。 UIButton *deleteButton = subview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } else { // iOS8-10: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView SignCell *tableCell = [self.mTableView cellForRowAtIndexPath:self.dataSource.editingIndexPath]; for (UIView *subview in tableCell.subviews) { if ( [subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 1) { UIButton *deleteButton = subview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } }
4、实现删除样式
- (void)configDeleteButton:(UIButton*)deleteButton { deleteButton.backgroundColor = kBgColor; [deleteButton setTitle:@"" forState:UIControlStateNormal]; [deleteButton setImage:[UIImage imageNamed:@"delete"] forState:UIControlStateNormal]; }
补充 / 扩展:
如果只想修改 tableView: editActionsForRowAtIndexPath: 里左滑按钮的frame、圆角,可以单独在cell里重写以下方法即可(只测了iOS10,其他版本未知)。不过总宽度在这里好像修改不了。
参考自 https://blog.csdn.net/klshuo/article/details/51305256
- (void)didTransitionToState:(UITableViewCellStateMask)state{ [super didTransitionToState:state]; if ((state & UITableViewCellStateShowingDeleteConfirmationMask) == UITableViewCellStateShowingDeleteConfirmationMask) { dispatch_async(dispatch_get_main_queue(), ^{ //必须在主线程 for (UIView* subview in self.subviews) { if ([NSStringFromClass([subview class]) isEqualToString:@"UITableViewCellDeleteConfirmationView"]) { subview.backgroundColor = [UIColor clearColor]; int i = 0; for (UIButton* subsubview in subview.subviews) { subsubview.titleLabel.font = [UIFont systemFontOfSize:14.0]; subsubview.frame = CGRectMake(15 + (18+40)*i, (kHomeCellHeight - 40) /2, 40, 40); subsubview.layer.cornerRadius = 20; subsubview.layer.masksToBounds = YES; i++; } } } }); } }
六、自定义长按手势拖动
参考自简书 《 iOS UITableView拖动排序 》 -- 最强的小强
0、写在前面
之前一直以为,tableview 拖动排序动画,需要用代理的方法才有动画,没想到,不用在代理中调用,也有动画。。。
1、添加手势
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressRecognizer:)]; longPress.minimumPressDuration = 0.6; [self.mTableView addGestureRecognizer:longPress];
2、手势动画
- (void)longPressRecognizer:(UILongPressGestureRecognizer*)longPress { //获取长按的点及cell CGPoint location = [longPress locationInView:self.mTableView]; NSIndexPath *indexPath = [self.mTableView indexPathForRowAtPoint:location]; UIGestureRecognizerState state = longPress.state; static UIView *snapView = nil; static NSIndexPath *sourceIndex = nil; switch (state) { case UIGestureRecognizerStateBegan:{ if (indexPath) { // 保存起始indexpath sourceIndex = indexPath; HomeCell *cell = [self.mTableView cellForRowAtIndexPath:indexPath]; // 截图 snapView = [self customViewWithTargetView:cell]; // 手指长按,弹出截图动画 __block CGPoint center = cell.center; snapView.center = center; snapView.alpha = 0.0; [self.mTableView addSubview:snapView]; [UIView animateWithDuration:0.1 animations:^{ center.y = location.y; snapView.center = center; snapView.transform = CGAffineTransformMakeScale(1.05, 1.05); snapView.alpha = 0.5; cell.alpha = 0.0; }]; } } break; case UIGestureRecognizerStateChanged:{ // 手指长按的截图,中心点,随着改变 CGPoint center = snapView.center; center.y = location.y; snapView.center = center; // 判断做 exchange 动画 HomeCell *cell = [self.mTableView cellForRowAtIndexPath:sourceIndex]; cell.alpha = 0.0; if (indexPath && ![indexPath isEqual:sourceIndex]) { [self.dataSource.dataSource exchangeObjectAtIndex:indexPath.row withObjectAtIndex:sourceIndex.row]; [self.mTableView moveRowAtIndexPath:sourceIndex toIndexPath:indexPath]; sourceIndex = indexPath; } } break; default:{ // 完成、或取消、或电话来临被迫退出手势、或其他 // 做动画,并保存 HomeCell *cell = [self.mTableView cellForRowAtIndexPath:sourceIndex]; [UIView animateWithDuration:0.25 animations:^{ snapView.center = cell.center; snapView.transform = CGAffineTransformIdentity; snapView.alpha = 0.0; cell.alpha = 1.0; } completion:^(BOOL finished) { [snapView removeFromSuperview]; snapView = nil; }]; sourceIndex = nil; NSLog(@"%@",@"做数据保存,本地,或网络"); } break; } }
七、仅做了解
1、cell 异步加载网络图片,主线程更新UI。
1)、现在有了 SDWebImage ,只做为一种思路了解
2)、重用机制。
在取 cell 的同时刷新 imageView 的 tag ,当 imageView 异步获取到图片,判断自己的 tag 还是不是请求前传进来的 index + basetag。
如果不加这样的判断,当网络差,会出现图片错位的情况。
3)、cacheDic。
目前写法,只是一个可变字典。
往后考虑,1)、获取图片前,先判断自定义缓存NSCache(或者字典)是否有相应url名的图片。
1)、有 -> 直接调用
2)、没有 -> 去本地查找,url名的图片
1)、有,提取到缓存NSCache。key = url,object = image。调用
2)、没有?请求,以url命名保存到本地、缓存,调用。
2)、NSCache设置一定大小,会自动删除旧。如缓存读不到,又会去本地读取,并刷新到NSCache里。
缓存缺陷,如果后台更新图片,且名字用原来的,就不会被刷新。
0、宏定义 #define kImageBaseTag 2000 1、判断数据 // 更新imageView的标签 imgView.tag = indexPath.row + kImageBaseTag; // 在单元格显示的时候,先清掉 imgView.image = nil; // 判断是否加载过,有就用,没有就请求 if ([[self.imageCacheDic allKeys] containsObject:self.dataSource[indexPath.row]]) { imgView.image = [self.imageCacheDic objectForKey:indexPath]; }else{ dispatch_async(_queue, ^{ NSURL *url = [NSURL URLWithString:self.dataSource[indexPath.row]]; [imgView requestImgFromUrl:url cache:self.imageCacheDic index:indexPath]; }); } 2、请求数据 #import "UIImageView+MyWebCache.h" -(void)requestImgFromUrl:(NSURL*)url cache:(NSMutableDictionary*)cache indexPath:(NSIndexPath*)indexPath { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setURL:url]; [request setHTTPMethod:@"GET"]; [request setTimeoutInterval:3]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { UIImage *image = [UIImage imageWithData:data]; dispatch_async(dispatch_get_main_queue(), ^{ if (image != nil) { // 添加到缓存 [cache setObject:image forKey:indexpath]; // 判断是否需要刷新 if (self.tag == indexPath.row + kBaseImageTag) { self.image = image; } } }); }] resume]; }
2、自定义循环池
NSMutableSet *recyclePool; MyCell *cell = [self.recyclePool anyObject]; if (cell) { // 从循环池内取出 [self.recyclePool removeObject:cell]; } else { // 创建 cell = [MyCell cell]; } // 超出屏幕再 add 进循环池
3、图片缓存相关
参照《iOS:图片相关》