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:图片相关》

 

 

 

 

 

 

 

posted on 2017-10-10 14:32  leonlincq  阅读(1942)  评论(0编辑  收藏  举报