内容目录
- UITableView初始化/头视图/编辑模式
- 代理方法及常用方法逻辑
- cell的重用原理(重难点)
- UITableView&UITableViewCell的常见设置
- 数据刷新方法及原则
- UITabelView性能优化
Declaration
@interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>
可以看到UITableView是继承于UIScrollView,而UIScrollView继承与UIView,所以父类的方法都是可以使用的,最常用的是使用ScrollView的移动就触发的代理方法-(void)scrollViewDidScroll:(UIScrollView *)scrollView;
初始化
通过懒加载展示初始化的基本流程,
- (UITableView *)tableView{
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; //初始化
_tableView.dataSource = self; //代理方法
_tableView.delegate = self; //代理方法
_tableView.rowHeight = 44; //设置行高
_tableView.showsVerticalScrollIndicator = NO; //不出现滚动条
_tableView.backgroundColor = [UIColor redColor]; //设置背景颜色
// 分割线颜色
_tableView.separatorColor = [UIColor redColor];
// 隐藏分割线
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return _tableView;
}
头视图设置
第一种
self.tableView.tableHeaderView=self.imageView;
第二种
self.tableView.contentInset=UIEdgeInsetsMake(200, 0, 0, 0);
代理方法
上面初始化赋值的两个代理对应<UITableViewDelegate,UITableViewDataSource>
,UITableViewDataSource
中有两个必须实现的方法numberOfRowsInSection,cellForRowAtIndexPath
,协议常用方法如下
/**
* 告诉tableView一共有多少组数据(设置分组数量)
*/
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
/**
* 告诉tableView第section组有多少行(设置每组cell的数量)
*/
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
/**
* 告诉tableView第indexPath行显示怎样的cell(cell内容)
*/
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
/**
* 告诉tableView第section组的头部标题
*/
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
/**
* 告诉tableView第section组的尾部标题
*/
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
/**
* 点击tableView的cell触发的点击事件方法
*/
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
/**
* 设置tableView每行的高度(一些需要动态计算高度的cell可以用这个方法去做最后的高度处理)
*/
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
/**
* 设置行高(标题Header的行高)
*/
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
/**
* 设置Header内容
*/
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;
/**
* 设置行高(标题Footer的行高)
*/
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;
/**
* 设置Footer内容
*/
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;
/**
* 记录上一次的点击效果(延迟点击)
*/
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath;
代理方法常用逻辑
根据Index获取当前的cell,这里以collectionView方法为例
#pragma mark collectionView的点击方法
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
//CollectionView的联动方法,让bodyCollectionView随点击联动,有动画
[self.bodyCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionNone animated:YES];
//判断是否为titleCollectionView
if(collectionView==self.titleCollectionView){
//获取当前的cell
GY_TitleCollectionViewCell *cell = (GY_TitleCollectionViewCell *)[self.titleCollectionView cellForItemAtIndexPath:indexPath];
//遍历cell里的四图片
for (int i = 0; i < 4; i++) {
if (indexPath.row == i) {
//高亮,高亮颜色
cell.label.highlighted=YES;
cell.label.highlightedTextColor=[UIColor orangeColor];
cell.label.backgroundColor=[UIColor colorWithRed:13/255.0 green:28/255.0 blue:40/255.0 alpha:1];
}
else
{
//如果不是点击的那张位置的图片,设置为灰色,注意:indexpath是自定义的(系统的indexPath为CollectionView的个数),自定义的是当前CollectionView,对应的cell
NSIndexPath *indexpath = [NSIndexPath indexPathForItem: i inSection:0];
GY_TitleCollectionViewCell *cell = (GY_TitleCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexpath];
cell.label.highlighted=NO;
cell.label.highlightedTextColor=[UIColor orangeColor];
cell.label.backgroundColor=[UIColor colorWithRed:13/255.0 green:29/255.0 blue:53/255.0 alpha:1];
}
}
}
}
根据当前偏移量设置UI(该逻辑为卡在64导航栏高度的位置)
#pragma mark --------移动就触发的方法
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (scrollView == self.tableView) {
CGFloat y0ffest=self.tableView.contentOffset.y;
NSLog(@"%f",y0ffest);
if (self.tableView.contentOffset.y<=-250) {
self.heroImage.frame =CGRectMake(250+y0ffest, y0ffest, self.tableView.frame.size.width-(250+y0ffest)*2, -y0ffest);
self.effectView.frame=CGRectMake(250+y0ffest, y0ffest, self.tableView.frame.size.width-(250+y0ffest)*2, -y0ffest);
self.displayNameLabel.frame=CGRectMake(20-250-y0ffest, 70-250-y0ffest, 120, 40);
self.titleLabel.frame=CGRectMake(20-250-y0ffest, 105-250-y0ffest, 40, 35);
}
else if(self.tableView.contentOffset.y >=-64){
self.heroImage.frame = CGRectMake(0, -250+y0ffest+64, self.tableView.frame.size.width, 250);
self.tableView.contentInset=UIEdgeInsetsMake(64, 0, 0, 0);
}
else{
self.displayNameLabel.alpha=1-0.005*(y0ffest+290);
self.titleLabel.alpha=1-0.005*(y0ffest+290);
}
}
}
根据cell自动计算高度
//设置cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
RedPersonCell *cell = (RedPersonCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
return cell.height;
}
取消cell选中状态
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// 不加此句时,在二级栏目点击返回时,此行会由选中状态慢慢变成非选中状态。
// 加上此句,返回时直接就是非选中状态。
}
UITableViewCell
这个控件是系统为我们提供的api,是一个提供给你调用快捷开发的api,结构及默认形态如下图
UITableViewCell
当然,如果这个控件满足不了UI的需求,就需要自定义cell了!
重用池机制
问题来了:iOS设备的内存有限,如果用UITableView显示成千上万条数据,就需要成千上万个UITableViewCell对象的话,那将会耗尽iOS设备的内存。要解决该问题,需要重用UITableViewCell对象
重用原理:当滚动列表时,部分UITableViewCell会移出窗口,UITableView会将窗口外的UITableViewCell放入一个对象池中,等待重用。当UITableView要求dataSource返回UITableViewCell时,dataSource会先查看这个对象池,如果池中有未使用的UITableViewCell,dataSource会用新的数据配置这个UITableViewCell,然后返回给UITableView,重新显示到窗口中,从而避免创建新对象
还有一个非常重要的问题:有时候需要自定义UITableViewCell(用一个子类继承UITableViewCell),而且每一行用的不一定是同一种UITableViewCell,所以一个UITableView可能拥有不同类型的UITableViewCell,对象池中也会有很多不同类型的UITableViewCell,那么UITableView在重用UITableViewCell时可能会得到错误类型的UITableViewCell
解决方案:UITableViewCell有个NSString *reuseIdentifier属性,可以在初始化UITableViewCell的时候传入一个特定的字符串标识来设置reuseIdentifier(一般用UITableViewCell的类名)。当UITableView要求dataSource返回UITableViewCell时,先通过一个字符串标识到对象池中查找对应类型的UITableViewCell对象,如果有,就重用,如果没有,就传入这个字符串标识来初始化一个UITableViewCell对象
Cell重用实现代码
/**
* 什么时候调用:每当有一个cell进入视野范围内就会调用
*/
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 0.重用标识
// 被static修饰的局部变量:只会初始化一次,在整个程序运行过程中,只有一份内存
static NSString *ID = @"cell";
// 1.先根据cell的标识去缓存池中查找可循环利用的cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
// 2.如果cell为nil(缓存池找不到对应的cell)
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
// 因为对于一个cell这里只会走一次,是初始化cell的方法,因为cellForRowAtIndexPath在滑动时会被频繁调用,所以可以在这里面写只想运行一次的代码
}
// 3.覆盖数据(需要动态设置的值在这里处理)
cell.textLabel.text = [NSString stringWithFormat:@"testdata - %zd", indexPath.row];
return cell;
}
另一种注册cell方法
// 在这个方法中注册cell
- (void)viewDidLoad {
[super viewDidLoad];
// 注册某个标识对应的cell类型
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:ID];
}
UITableViewCell的常见设置
// 取消选中的样式
cell.selectionStyle = UITableViewCellSelectionStyleNone;
// backgroundView的优先级 > backgroundColor
// 设置选中的背景色
UIView *selectedBackgroundView = [[UIView alloc] init];
selectedBackgroundView.backgroundColor = [UIColor redColor];
cell.selectedBackgroundView = selectedBackgroundView;
// 设置默认的背景色
cell.backgroundColor = [UIColor blueColor];
// 设置默认的背景色
UIView *backgroundView = [[UIView alloc] init];
backgroundView.backgroundColor = [UIColor greenColor];
cell.backgroundView = backgroundView;
// 设置指示器(显示在视图右边, 有>箭头,✔️等等效果)
// cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.accessoryView = [[UISwitch alloc] init];
自定义cell
创建一个继承自UITableViewCell的子类,比如MyCell
#import <UIKit/UIKit.h>
//父类是UIView 不是UITableView
@interface MyCell : UITableViewCell
//cell本身就提供了三个属性视图,所以为了避免冲突,一定不要自定义的cell的属性名和系统的冲突
//姓名和联系方式的label
@property(nonatomic,retain)UILabel *namelabel;
@property(nonatomic,retain)UILabel *phonelabel;
@end
//要实现自定义的cell,一般重写两个方法
//一.初始化方法
-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
[super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self createView];
}
return self;
}
-(void)createView{
//在里面只创建视图,不设置尺寸
self.namelabel=[[UILabel alloc] init];
self.namelabel.backgroundColor=[UIColor yellowColor];
//把属性视图放到contentView上显示
[self.contentView addSubview:self.namelabel];
self.phonelabel=[[UILabel alloc] init];
self.phonelabel.backgroundColor=[UIColor cyanColor];
[self.contentView addSubview:self.phonelabel];
}
//重写第二个方法
//这个方法是整个cell在出现前所执行的最后一个方法,所以为了能精准的设置它的尺寸,在这个方法里写空间尺寸的设置
-(void)layoutSubviews{
//如果不写,布局可能会出现问题,所以别忘记了!!!!!!
[super layoutSubviews];
//在这个方法里只设置尺寸
self.namelabel.frame = CGRectMake(0, 0, WIDTH/2, HEIGHT/2);
self.phonelabel.frame = CGRectMake(WIDTH/2, HEIGHT/2, WIDTH/2, HEIGHT/2);
}
编辑模式
//开启一下tableView的编辑模式
[self.tableView setEditing:YES animated:YES];
#pragma mark 设置点击进入编辑模式!
//重写一下系统提供的编辑按钮的点击方法
-(void)setEditing:(BOOL)editing animated:(BOOL)animated{
[super setEditing:editing animated:animated];
[self.tableView setEditing:editing animated:animated];
}
#pragma mark 逐行的去设置,那些行编辑,哪些不行编辑
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{
return YES;
}
#pragma mark 设置编辑的模式
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
// return UITableViewCellEditingStyleInsert|UITableViewCellEditingStyleDelete;
//默认是红色 - 号,Insert是绿色加号,如果+ -取异或,变为多选圆圈!
return UITableViewCellEditingStyleDelete;
}
#pragma mark 设置删除按钮的标题
-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath{
return @"来点我啊";
}
#pragma mark 点击方法,实现左划效果,而且是对应按钮功能的点击方法
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(@"编辑开始");
//先判断当前的编辑模式
if (editingStyle==UITableViewCellEditingStyleDelete) {
//先把数组里的对象删除掉
[self.stuArr removeObjectAtIndex:indexPath.row];
//第一种方式
//[self.tableView reloadData];
//第二种方式 系统自带的删除方法,可以选择删除动画
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
}
}
#pragma mark 移动
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
//它只是视觉上移动,并没有改变数组的顺序,所以想要改变数组的顺序,需要用代码来实现
//获取要移动的数据
Student *stu=[self.stuArr[sourceIndexPath.row]retain];
//在数组里把这个对象移除掉
[self.stuArr removeObjectAtIndex:sourceIndexPath.row];
[self.stuArr insertObject:stu atIndex:destinationIndexPath.row];
[stu release];
}
//添加数组里面需要反向添加
- (nullable NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0){
UITableViewRowAction *actionFirst=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"删除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
NSLog(@"111");
}];
actionFirst.backgroundColor=[UIColor redColor];
UITableViewRowAction *actionTwo=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"标为未读" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
}];
actionTwo.backgroundColor=[UIColor orangeColor];
UITableViewRowAction *actionThree=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"置顶" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
}];
actionThree.backgroundColor=[UIColor grayColor];
return @[actionFirst,actionTwo,actionThree];
}
自定义删除按钮
#define CELL_HEIGHT (117.f)
#pragma mark 自定义编辑效果
//触发viewDidLayoutSubviews方法的调用
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.editingIndexPath = indexPath;
[self.view setNeedsLayout]; // 触发-(void)viewDidLayoutSubviews
}
- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
self.editingIndexPath = nil;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (self.editingIndexPath)
{
[self configSwipeButtons];
}
}
- (void)configSwipeButtons
{
// 获取选项按钮的reference
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0")) //iOS11以上系统走这个代理方法
{
// iOS 11层级 (Xcode 9编译): UITableView -> UISwipeActionPullView
for (UIView *subview in self.tableView.subviews)
{
if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subview.subviews count] >= 1)
{
UIView *swipeActionPullView = subview;
//修改背景颜色
swipeActionPullView.backgroundColor = [UIColor clearColor];
// 和iOS 10的按钮顺序相反
UIButton *deleteButton = subview.subviews[0];
[self configDeleteButton:deleteButton];
}
}
}
else
{
// iOS 8-10层级: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView
HYHwLinkCell *tableCell = [self.tableView cellForRowAtIndexPath:self.editingIndexPath];
for (UIView *subview in tableCell.subviews)
{
if ([subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 1)
{
UIView *swipeActionPullView = subview;
//修改背景颜色
swipeActionPullView.backgroundColor = [UIColor clearColor];
UIButton *deleteButton = subview.subviews[0];
[self configDeleteButton:deleteButton];
}
}
}
}
//自定义的按钮控件
- (void)configDeleteButton:(UIButton*)deleteButton
{
if (deleteButton)
{
deleteButton.frame = CGRectMake(0, 0, CELL_HEIGHT-10, CELL_HEIGHT-10); //设置宽.高相等的frame
[deleteButton setTitle:@"" forState:0]; //设置空不显示,用下面的label文字
deleteButton.layer.cornerRadius = 4;
deleteButton.layer.masksToBounds = YES;
deleteButton.backgroundColor = [UIColor colorWithHexValue:0xFA3232];
//添加label替换默认的文字
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, deleteButton.width, deleteButton.height)];
label.text = @"删除";
label.textColor = deleteButton.tintColor;
label.font = [UIFont systemFontOfSize:14];
label.textAlignment = 1;
[deleteButton addSubview:label];
}
}
数据刷新方法
重新刷新屏幕上的所有cell
[self.tableView reloadData];
单行刷新(刷新特定行的cell)
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
插入特定行数的cell
- (void)addRows{
NSMutableArray *indexPaths = [NSMutableArray array];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.arr.count-1 inSection:0];
[indexPaths addObject:indexPaths];
//这个位置应该在修改tableView之前将数据源先进行修改,否则会崩溃. 必须向tableView的数据源数组添加一条数据
[_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
[_tableView endUpdates];
}
删除特定行数的cell
- (void)removeRows{
NSMutableArray *indexPaths = [NSMutableArray arrayWithArray:self.arr];
[indexPaths removeObjectAtIndex:0];
//这个位置应该在修改tableView之前将数据源先进行修改,否则会崩溃
[_tableView beginUpdates];
[_tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationBottom];
[_tableView endUpdates];
}
数据刷新的原则
通过修改模型数据,来修改
tableView
的展示
先修改模型数据
再调用数据刷新方法
改模型 <==> 改界面
不要直接修改cell上面子控件的属性这样改了重用的时候不就傻逼了?
坑点
进入页面后发现scrollView起始点不对(很奇怪的问题)
//在进入当前controller 的时候,找第一个响应的scrollView,然后给这个是scrollview 一个 inset (上, 左, 下 , 右) 偏移
self.automaticallyAdjustsScrollViewInsets = NO;
TableView 刷新后调用 reloadData 出现上移
具体参照博客 https://www.jianshu.com/p/9f9bedadf144
UITableView优化
- 重用池 --- 避免频繁的对象创建
- 异步绘制 --- 耗时操作(如下载/图片加载)放在子线程避免主线程卡顿
- 减少离屏渲染 --- 如-shadows(阴影)-shouldRasterize(光栅化)-group opacity(不透明)-设置圆角 -渐变 等
- 单行刷新 --- 全局刷新很消耗性能
- 缓存cell高度 --- 避免重复计算cell高度
- 预加载cell --- 丝滑般滑动体验
具体请看下面博客
①iOS开发思路:UITableView预加载上拉数据
②仿安卓UITableview 预加载数据
参考博客
iOS开发-UITableView进阶
UITableView 的优化技巧
iOS开发——实战UITableview深度优化
网友评论