Objective-C的UITableView(表视图)学习笔记

UITableView - 表视图

UITableView是作为IOS中显示数据列表最常用的一个控件,继承UIScrollView,支持垂直滚动。拥有两种内置的样式,UITableViewStylePlain普通样式与UITableViewStyleGrouped分组样式。例如显示联系人应用程序,每个单独的单元格显示联系人的姓名,既可以将表视图配置为一个内容较长的列表(UITableViewStylePlain普通样式),也可以将相关单元格行分节到多个部分中(UITableViewStyleGrouped分组样式),以便更轻松地浏览内容。

UITableView在具有高度结构化或分层组织的数据的应用程序中很常见。包含分层数据的应用程序通常将表格与导航视图控制器结合使用,这有助于在层次结构的不同级别之间进行导航。例如Iphone中的设置应用程序使用表格和导航控制器来组织系统设置。

UITableView可以管理列表的基本外观,但由列表的单元格(UITableViewCell对象)显示实际内容,系统提供的标准单元格已经配置了显示文本和图像的简单组合,也可以通过自定义单元格显示自定义的任何内容

UITableView还可以通过为每一节单元格设置页眉和页脚视图来为单元格组提供附加信息。

UITableViewStylePlain普通样式:

充满年代感的截图1.png

UITableViewStyleGrouped分组样式:

充满年代感的截图2.png
UITableView常用属性
@property (nonatomic, readonly) UITableViewRowActionStyle style;

属性描述设置UITableView样式。此属性的值在创建时设置,以后无法更改。注:如果UITableView的style设置为UITableViewStyleGrouped样式,那么每节头部和每节尾部视图没有悬停效果。相反,如果UITableView的style设置为UITableViewStylePlain样式,那么每节头部和每节尾部视图会有悬停效果

@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;

属性描述设置充当UITableView的数据源的对象,数据源对象必须采用UITableViewDataSource协议。

@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;

属性描述设置充当UITableView代理的对象,代理对象必须采用UITableViewDelegate协议。

@property (nonatomic, getter=isEditing) BOOL editing;   

属性描述确定UITableView是否处于编辑模式的布尔值。当此属性的值为“YES”时,UITableView处于编辑模式:UITableView的单元格可能在每个单元格的左侧显示一个插入或删除控件,在右侧显示一个重新排序控件,具体取决于单元格的配置方式。点击控件将导致表视图调用数据源table view:committedingstyle:forRowAtIndexPath:方法。默认值为NO。

@property (nonatomic) separatorStyle API_UNAVAILABLE(tvOS);

属性描述设置UITableView单元格的分割样式。这个属性的值是UITableViewCell中描述的一个分隔符样式的常量,UITableView使用这个属性来设置tableView中委托返回的单元格上的分隔符样式。

  • UITableViewCellSeparatorStyle枚举的分隔符样式:
typedef NS_ENUM(NSInteger, UITableViewCellSeparatorStyle) {
    //没有分隔符
    UITableViewCellSeparatorStyleNone,
    //分隔符是一条横穿其宽度的单线。这是默认值
    UITableViewCellSeparatorStyleSingleLine,
    //分隔符是双线横跨其宽度,使其具有蚀刻外观,此样式当前仅支持分组样式表视图
    UITableViewCellSeparatorStyleSingleLineEtched API_DEPRECATED("Use UITableViewCellSeparatorStyleSingleLine for a single line separator.", ios(2.0, 11.0))
} API_UNAVAILABLE(tvOS);
@property (nonatomic, strong, nullable) UIColor *separatorColor UI_APPEARANCE_SELECTOR API_UNAVAILABLE(tvOS);

属性描述UITableView中每行单元格分隔符的颜色,默认颜色为灰色。

@property (nonatomic, strong, nullable) UIView *tableHeaderView;

属性描述设置显示在UITableView内容上方的视图,使用此属性可为整个UITableView指定头部标题视图。头部标题视图是出现在UITableView的滚动内容中的第一项,它与添加到各个节中的头部视图是分开的。此属性的默认值为nil。

将视图分配给此属性时,请将该视图的高度设置为非零值。UITableView只考虑视图的框架矩形的高度,它自动调整头部标题视图的宽度以匹配UITableView的宽度。

如图 :

截屏2022-09-05 14.56.09.png

:有时控制器导航栏透明的,表视图内容需要显示在状态栏之下,滚动视图的contentInsetAdjustmentBehavior属性,控制器的extendedLayoutIncludesOpaqueBars属性都设置无误时,表视图内容仍旧偏移,如图:

截屏2023-02-11 11.34.22.png

则可以尝试为该属性设置一个高度极小的视图以消除偏移:

self.tableView.tableHeaderView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 0, 0.1)];
截屏2023-02-11 11.47.22.png
@property (nonatomic, strong, nullable) UIView *tableFooterView;

属性描述设置显示在UITableView内容下方的视图。使用此属性指定整个UITableView的尾部标题视图。尾部标题视图是出现在UITableView的滚动内容中的最后一项,它与添加到各个节中的尾部视图是分开的。此属性的默认值为nil。

将视图分配给此属性时,请将视图的高度设置为非零值。UITableView只考虑视图的框架矩形的高度,它自动调整尾部标题视图的宽度以匹配UITableView的宽度。

@property (nonatomic) BOOL allowsSelection API_AVAILABLE(ios(3.0));

属性描述决定用户是否可以选中行的布尔值。如果此属性的值为“YES”(默认值),则用户可以选择行。如果将其设置为“NO”,则它们无法选择行。仅当UITableView未处于编辑模式时,设置此属性才会影响单元格选择。如果要在编辑模式下限制单元格的选择,请在编辑期间使用AllowSelectionDuringEditing。

@property (nonatomic) BOOL allowsSelectionDuringEditing; 

属性描述一个布尔值,用于确定用户在UITableView处于编辑模式时是否可以选择单元格。如果此属性的值为“YES”,则用户可以在编辑期间选择行。默认值为“NO”。如果要限制单元格的选择,而不考虑模式,请使用allowsSelection。

@property (nonatomic) BOOL allowsMultipleSelection API_AVAILABLE(ios(5.0));   

属性描述一个布尔值,用于确定用户是否可以在编辑模式之外选择多行。此属性控制是否可以在编辑模式之外同时选择多行,当此属性的值为“YES”时,被点击的每一行将获取选定的外观,再次点击该行将删除选定的外观。如果访问indexPathsForSelectedRows,则可以获取标识选定行的索引路径。此属性的默认值为“NO”。

@property (nonatomic) BOOL allowsMultipleSelectionDuringEditing API_AVAILABLE(ios(5.0)); 

属性描述一个布尔值,控制用户在编辑模式下是否可以同时选择多个单元格,此属性的默认值为NO。如果将其设置为YES,则在编辑模式下在所选行的旁边出现复选标记。此外,当UITableView进入编辑模式时,它不会查询编辑样式。如果访问indexPathsForSelectedRows,可以获得标识所选行的索引路径。

@property (nonatomic) CGFloat estimatedRowHeight API_AVAILABLE(ios(7.0));

属性描述UITableView中单元格的估计高度。提供单元格的高度的非负估计值可以提高加载UITableView的性能。如果UITableView包含可变高度单元格,则在加载表时计算它们的所有高度可能会很耗费资源,估算允许将几何计算的某些成本从加载时间推迟到滚动时间。

默认值是UITableViewAutomaticDimension,这意味着UITableView会选择一个估计的高度来代表使用。将值设置为0将禁用估计高度,这将导致UITableView请求每个单元格的实际高度。如果表使用自调整大小的单元格,则此属性的值不能为0

在使用高度估计时,UITableView会主动管理从滚动视图(UIScrollView)继承的内容contentoffset和contentSize属性,不要试图直接读取或修改这些属性。

@property (nonatomic) CGFloat estimatedSectionHeaderHeight API_AVAILABLE(ios(7.0));

属性描述表视图中每节头部视图的估计高度。与estimatedRowHeight使用类似。

@property (nonatomic) CGFloat estimatedSectionFooterHeight API_AVAILABLE(ios(7.0));

属性描述表视图中每节尾部视图的估计高度。与estimatedRowHeight使用类似。

@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForVisibleRows;

属性描述索引路径的数组,每个路径标识表视图中的一个可见行。此属性的值是一个NSIndexPath对象数组,每个对象表示行索引和节索引,它们一起标识表视图中的可见行。如果没有行可见,则值为nil。

\color{red}{例如获取最后一个可见单元格的代码片段:}

///获取最后一个可见单元格
- (YBPopupMenuCell *)getLastVisibleCell
{
    //获取索引路径的数组
    NSArray <NSIndexPath *>*indexPaths = [_contentView indexPathsForVisibleRows];
    //返回一个数组,该数组按升序列出接收数组的元素,由给定NSComparator块指定的比较方法确定。
    indexPaths = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *  _Nonnull obj1, NSIndexPath *  _Nonnull obj2) {
        return obj1.row < obj2.row;
    }];
    //获取排序后的索引路径的数组的第一个元素
    NSIndexPath *indexPath = indexPaths.firstObject;
    //返回指定索引处的单元格
    return [_contentView cellForRowAtIndexPath:indexPath];
}
UITableView常用函数
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;

函数描述初始化一个UITableView,并且设置UITableView样式,创建UITableView时必须指定其样式,以后不能更改该样式。如果使用UIView方法initWithFrame初始化表视图,则会将UITableViewStylePlain样式用作默认样式

参数 :

frame : 一个矩形,指定UITableView在其superview's坐标中的初始位置和大小。随着UITableView单元格的添加和删除,UITableView的frame也随之更改。

style :指定UITableView样式的常量。

返回值 : 返回初始化的UITableView对象。

- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath;

函数描述 :返回指定索引路径处的UITableView的单元格

参数 :

indexPath : 在UITableView中定位单元格的索引路径。

返回值 :表示UITableView的单元格的对象,如果单元格不可见或indexPath超出范围,则为nil。

- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

函数描述通过identifier标识在(缓存)池中找到对应的UITableViewCell(单元格)对象

出于性能原因,UITableView的数据源通常应该在为其tableView:cellForRowAtIndexPath:函数中的行分配单元格时重用UITableViewCell(单元格)对象。UITableView维护数据源标记为可重用的UITableViewCell(单元格)对象的队列或列表。当要求为UITableView提供新UITableViewCell(单元格)对象时,从数据源对象调用此方法。如果现有的UITableViewCell(单元格)对象可用,或者使用以前注册的类或NIB文件,该方法将对现有的UITableViewCell(单元格)对象进行出列。如果没有可重用的UITableViewCell(单元格)对象,并且您没有注册类或nib文件,则此方法返回nil。

如果您为指定的标识符注册了一个类,并且必须创建一个新的UITableViewCell(单元格)对象,则此方法通过调用其initWithStyle:reuseIdentifier:函数初始化该UITableViewCell(单元格)对象。对于基于nib的单元,此方法从提供的nib文件加载UITableViewCell(单元格)对象。如果现有的单元可用于重用,则该方法调用UITableViewCell(单元格)对象的PraseReFuleRead方法。

参数 :

identifier : 标识要重用的UITableViewCell(单元格)对象的字符串,此参数不能为nil。

返回值 : 个带有关联标识符的UITableViewCell(单元格)对象,如果可重用单元格队列中不存在这样的对象,则为nil。

- (void)setEditing:(BOOL)editing animated:(BOOL)animated;

函数描述是否开启编辑模式。当在editing属性值设置为YES时调用此方法时,UITableView将通过在每个可见的UITableViewCell(单元格)对象上调用setEditing:animated:进入编辑模式。在编辑设置为NO时调用此方法将关闭编辑模式。在编辑模式下,UITableView的单元格可能在每个单元格的左侧显示一个插入或删除控件,在右侧显示一个重新排序控件,具体取决于单元格的配置方式。UITableView的数据源可以通过实现table view:canEditRowAtIndexPath:从编辑模式中选择性地排除单元格。

参数 :

editing :YES,进入编辑模式;NO,退出编辑模式。默认值为NO。

animated : YES,以动画方式转换到编辑模式;NO,以立即转换。

UITableView刷新的方式
//全表刷新
- (void)reloadData;

函数描述重新访问数据源,刷新界面。调用此方法重新加载用于构造UITableView的所有数据,包括单元格、分节头部视图和分节尾部视图、索引数组等。为了提高效率,UITableView只重新显示那些可见的行。如果由于重新加载而导致表收缩,则它将调整偏移。

UITableView的代理或数据源在希望UITableView完全重新加载其数据时调用此方法。不应该在插入或删除单元格的方法中调用它,特别是在通过调用beginUpdates和endUpdates实现的动画块中

当hasUncommittedUpdates属性为YES时,不要调用此方法。这样做会强制UITableView在重新加载数据之前删除任何未提交的更改。

整个tableview的刷新,例如:

[self.tableView reloadData];

注:reloadData 并不会等待tableview更新结束后才返回,而是立即返回,然后去计算表高度,获取cell等。如果表中的数据非常大,在一个Runloop周期没执行完,这时需要tableview视图数据的操作就会出问题了

\color{red}{延迟到reloadData结束在操作:}

方法一 : layoutIfNeeded会强制重绘并等待完成。
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
//刷新完成
方法二: reloadData会在主队列执行,而 dispatch_get_main_queue 会等待机会,直到主队列空闲才执行。
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
    //刷新完成
});
//刷新某节中的某行单元格
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation API_AVAILABLE(ios(3.0));

函数描述使用给定的动画效果重新加载指定节中指定行的单元格

重新加载指定行的单元格将导致UITableView向其数据源请求该行的新单元格。该UITableView将新单元格导入,同时将旧单元格导出。如果想要警告用户单元格的值正在更改,请调用此方法,但是如果通知用户并不重要(也就是说只想更改一个单元格正在显示的值),那么您可以获取特定行的单元格并设置它的新值。

在beginUpdates和endpdates方法定义的animation块中调用此方法时,其行为类似于deleteRowsAtIndexPaths:withRowAnimation:。UITableView传递给方法的索引是在任何更新之前在UITableView的状态中指定的,这与动画块中的插入、删除和重新加载方法调用的顺序无关。

参数 :

indexPaths : 标识要重新加载的行的NSIndexPath对象数组。

animation : 指示如何设置重新加载动画的常数,例如,从底部淡出或滑出。

tableview刷新某节中的某行单元格,例如:

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
        
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath,nil] withRowAnimation:UITableViewRowAnimationFade];
//刷新某节中的单元格
\- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation API_AVAILABLE(ios(3.0));

函数描述使用给定的动画效果重新加载指定节中的单元格

调用此方法将导致UITableView向其数据源询问指定节中的新单元格。UITableView在设置旧单元格的动画时设置新单元格插入的动画。如果要提醒用户指定节的值正在更改,请调用此方法。但是如果只想更改指定节的单元格中的值而不通知用户,则可以获取这些单元格并直接设置它们的新值。

在beginUpdates和endpdates方法定义的animation块中调用此方法时,其行为类似于deleteSections:withRowAnimation:。UITableView传递给方法的索引是在任何更新之前在UITableView的状态中指定的,这与动画块中的插入、删除和重新加载方法调用的顺序无关。

参数 :

sections : 标识要重新加载的组的索引集。

animation : 指示如何设置重新加载动画的常数,例如,从底部淡出或滑出。

tableview刷新某节中的单元格,例如:

NSIndexSet *indexSet = [[NSIndexSet alloc] initWithIndex:1];
[self.tableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationFade];
  • 操作Cell系统提供的动画效果:
typedef NS_ENUM(NSInteger, UITableViewRowAnimation) {
        UITableViewRowAnimationFade,//动画淡入淡出
        UITableViewRowAnimationRight,//动画向右滑出
        UITableViewRowAnimationLeft,//动画向左滑出
        UITableViewRowAnimationTop,//动画向上滑出
        UITableViewRowAnimationBottom,//动画向下滑出
        UITableViewRowAnimationNone,//无动画效果
        UITableViewRowAnimationMiddle,//动画中间滑出
        UITableViewRowAnimationAutomatic = 100//自动选择动画
    };
针对执行reloadRowsAtIndexPaths/reloadSections/reloadData方法出现界面跳动问题

针对执行reloadRowsAtIndexPaths/reloadSections/reloadData方法出现界面跳动问题,可采用下面代码中尝试处理,尤其是有自适应cell高度的。

if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        self.tableView.estimatedRowHeight = 0;
        self.tableView.estimatedSectionHeaderHeight = 0;
        self.tableView.estimatedSectionFooterHeight = 0;
    }

对于刷新某Section中的某Row时,页面跳动的现象:

Jietu20210415-151422.gif

也可以尝试采用禁用视图转换动画的方式消除跳动,代码如下:

//刷新表视图
[UIView performWithoutAnimation:^{
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:idx inSection:0];
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
UITableView的编辑模式

UITableView有个editing属性,设置为YES时,可以进入编辑模式。在编辑模式下,可以管理表格中的行,比如改变行的排列顺序,增加行,删除行,但不能修改行的内容。

开启编辑模式的方式:

//通过属性
@property(nonatomic,getter = isEditing)Bool editing;
//通过函数
-(void)setEditing:(Bool)editing   animated:(Bool)animated
删除UITableView的行
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;

函数描述删除由索引路径数组指定的单元格,并使用一个选项设置删除的动画。

当这个方法在一个由beginUpdates和endpdates方法定义的动画块中被调用时,UITableView会延迟任何行或节的插入,直到它处理了行或节的删除之后。无论插入和删除方法调用的顺序如何,都遵循此顺序,这与在可变数组中插入或删除项不同,在可变数组中,操作可以影响用于后续插入或删除操作的数组索引。

参数 :

indexPaths : 标识要删除的行的NSIndexPath对象数组。

animation : 指示如何设置删除动画的常数,例如,从底部淡出或滑出。

\color{red}{例如:删除一行单元格:}

  • 1.首先要开启编辑模式。
  • 2.实现UITableViewDataSource的如下方法。
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
    if(editingStyle == UITableViewCellEditingStyleDelete){
        //删除真实数据
        [self.data removeObjectAtIndex:indexPath.row];
        //删除UITableView中的某一行(带动画效果)
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationLeft];
        //如不考虑动画效果,也可以直接reloadData,但不建议这么做。
        [tableView reloadData];
    }
}
移动UITableView的行
  • 1.首先要开启编辑模式。
  • 2.实现UITableViewDateSource的如下方法(如果没有实现此方法,将无法换行)。

\color{red}{例如:移动一行单元格:}

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    NSInteger from = sourceIndexPath.row;
    NSInteger to = destinationIndexPath.row;
    if(from == to){
        return;
    }
     //交互数据
    [self.data exchangeObjectAtIndex:from withObjectAtIndex:to];
}
选中UITableView的行

当某行被选中时会调用此方法(UITableViewDelegate的方法)

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    //取消选中某行时,让被选行的高亮颜色消失(带动画效果)
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

UITableViewRowAction - UITableView某行单元格中水平滑动时要显示的单个操作

UITableViewRowAction,当用户在UITableView某行单元格中水平滑动时要显示的单个操作

创建一个UITableViewRowAction对象来为UITableView某行定义一个单独的自定义操作。用户在UITableView中水平滑动以显示与一行单元格相关的操作。每个row-action对象包含文本显示、要执行的操作以及要应用于该操作的任何特定格式。

要向UITableView的某行单元格添加自定义操作,请在表视图的代理对象中实现tableView:editActionsForRowAtIndexPath:方法。在该方法中,创建并返回指定行的单元格操作。该UITableView显示您的操作按钮,并在用户轻击其中一个按钮时执行相应的处理程序块。

在ios11及以后版本中,使用UISwipeActionsConfiguration来配置表视图操作

UITableViewRowAction的属性与函数
\+ (instancetype)rowActionWithStyle:(UITableViewRowActionStyle)style title:(nullable NSString *)title handler:(void (^)(UITableViewRowAction *action, NSIndexPath *indexPath))handler;

函数描述创建并返回一个新的UITableView某行单元格操作对象,以后无法更改指定的样式和处理程序块,但可以更改操作按钮的标题。还可以使用此类的属性配置按钮的其他外观相关属性。可以将同一行单元格操作对象(UITableViewRowAction)指定给UITableView的多行单元格。

参数 :

style : 应用于按钮的样式特征。使用此值可将默认外观特征应用于按钮。这些特征提供了有关按钮功能的信息。例如使用它来指示某个操作对底层数据具有破坏性。

title : 要在按钮中显示的字符串,指定用户当前语言的本地化字符串。

handler : 当用户点击与此操作相关联的按钮时要执行的块。UIKit会复制您提供的块,当用户选择此对象表示的操作时,UIKit将在应用程序的主线程上执行处理程序块。此参数不能为nil。此块没有返回值,并接受以下参数:
action : 表示用户选择的操作的操作对象。
indexPath : 用户操作的UITableView某行单元格索引。

返回值 : 可以从UITableView的代理方法返回的新UITableView某行单元格的操作对象。

  • UITableViewRowActionStyle枚举值如下:
typedef NS_ENUM(NSInteger, UITableViewRowActionStyle) {
    UITableViewRowActionStyleDefault = 0, //将默认样式应用于按钮。
    UITableViewRowActionStyleDestructive = UITableViewRowActionStyleDefault, //等于默认样式。
    UITableViewRowActionStyleNormal //应用反映标准的非破坏性操作的样式。
} API_DEPRECATED("Use UIContextualAction and related APIs instead.", ios(8.0, 13.0)) API_UNAVAILABLE(tvOS);
@property (nonatomic, readonly) UITableViewRowActionStyle style;

属性描述 : 应用于操作按钮的样式。此属性的值在创建时设置,以后无法更改。

@property (nonatomic, copy, nullable) NSString *title;

属性描述 : 操作按钮的标题。

@property (nonatomic, copy, nullable) UIColor *backgroundColor; 

属性描述 : 操作按钮的背景色,使用此属性指定按钮的背景色。如果未为此属性指定值,则UIKit将根据“style”属性中的值指定默认颜色。

UITableViewDataSource(数据源)与UITableViewDelegate(代理)

UITableView需要一个数据源(dataSource)来显示数据,UITableView会向数据源查询一共有多少行数据以及每一行显示什么数据等。没有设置数据源的UITableview只是个空壳,凡是遵守UITableViewDataSource协议的Object-c对象都可以作为UITableView的数据源。

通常都要为UITableView设置代理(delegate),以便在UITableView触发事件时作出相应的处理,比如选中了某一行。凡是遵守了UITableViewDelegate协议的Object-c对象,都可以是UITableView的代理对象。

一般情况下会让控制器充当UITableView的数据源和代理。

UITableViewDataSource提供的一些方法
//共分为多少组数据
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
}

函数描述询问数据源共分为多少节数据,如果未实现,则默认值为1节数据。

参数 :

tableView : 表示请求此信息的表视图的对象。

返回值 : tableView中数据分节的数量。

//每组有多少行数据
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
}  

函数描述询问数据源每节有多少行数据

参数 :

tableView : 请求此信息的表视图对象。

section : 表视图中标识节的索引号。

返回值 : 每节中的行数。

//每行显示的内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
}

函数描述在实现中,为给定的索引路径创建并配置适当的单元格。使用表视图的dequeueReusableCellWithIdentifier:forIndexPath:函数创建单元格,该方法回收或创建单元格。

创建单元格后,使用适当的数据值更新单元格的属性。不要自己调用这个方法,如果希望从表中检索单元格,请调用表视图的cellForRowAtIndexPath:方法。

参数 :

tableView : 请求单元格的表视图对象。

indexPath : 在tableView中定位行的索引路径。

返回值 :从UITableViewCell继承的对象,表视图可用于指定行。如果返回nil,UIKit将引发断言。

//某一节的头部标题
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
}

函数描述向数据源询问UITableView的指定节的头部标题。表格视图对节标题使用固定字体样式。

如果需要其他字体样式,请改为在委托方法tableView:viewForHeaderInSection:中返回自定义视图(例如,UILabel对象)。

如果未实现此方法或tableView:viewForHeaderInSection:函数,则表不显示节的标题。如果同时实现这两个方法,则tableView:viewForHeaderInSection:函数优先。

参数 :

tableView : 请求标题的表视图对象。

section :标识tableView的每节的索引号。

返回值 : 用作每节头部标题的字符串。如果您返回nil,则该部分将没有标题。

//某一节的尾部标题
-(NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section{
}

函数描述向数据源询问表视图的指定节的尾部标题。表视图对节页脚标题使用固定字体样式。

如果需要其他字体样式,请改为在委托方法tableView:viewForFooterInSection:中返回自定义视图(例如,UILabel对象)。

如果未实现此方法或tableView:viewForFooterInSection:方法,则表不显示节的页脚。如果同时实现这两个方法,则tableView:viewForFooterInSection:函数优先。

参数 :

tableView : 请求标题的表视图对象。

section :标识tableView的每组的索引号。

返回值 : 用作每节尾部标题的字符串。如果您返回nil,则该部分将没有标题。

//某一行是否可以编辑
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{
}

函数描述询问数据源某一行单元格是否可以编辑。该方法允许数据源将个别行的单元格排除在可编辑之外。

可编辑的行显示其单元格中的插入或删除控件。如果未实现此方法,则假定所有行都是可编辑的。不可编辑的行忽略UITableViewCell对象的editingStyle属性,对于删除或插入控件不执行缩进。

可编辑但不希望显示插入或移除控件的行可以从tableView:editingStyleForRowAtIndexPath:delegate方法返回UITableViewCellEditingStyleNone。

参数 :

tableView : 请求此信息的表视图对象。

indexPath :在tableView中定位行的索引路径。

返回值 : 如果indexPath指示的行的单元格是可编辑的,则为“YES”;否则为“NO”。

- (nullable NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath API_DEPRECATED_WITH_REPLACEMENT("tableView:trailingSwipeActionsConfigurationForRowAtIndexPath:", ios(8.0, 13.0)) API_UNAVAILABLE(tvOS);

函数描述请求代理在响应指定行的单元格滑动时显示操作。当希望为表某个行的单元格提供自定义操作时,请使用此方法。当用户水平地在一行单元格中滑动时,表视图将行内容移到一边,以显示您的操作。单击其中一个操作按钮将执行与操作对象一起存储的处理程序块。如果不实现此方法,则在用户滑动该行时,table视图将显示标准的附件按钮。

参数 :

tableView :请求此信息的表视图对象。

indexPath :行的索引路径。

返回值 :UITableViewRowAction对象的数组,代表行的动作。您提供的每个操作都用于创建用户可以点击的按钮。

//某一行是否可以移动来进行重新排序
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath{
}

函数描述询问数据源是否可以将给定行的单元格移动到UITableView中的其他位置。此方法允许数据源指定不显示指定行的单元格重新排序控件,默认情况下,如果数据源实现tableView:moveRowAtIndexPath:toIndexPath:函数,则显示重新排序控件。

参数 :

tableView : 请求此信息的表视图对象。

indexPath :在tableView中定位行的索引路径。

返回值 :如果行可以移动,则为“YES”;否则为“NO”。

//当用户点击与表视图中的UITableViewCell对象相关联的插入(绿色加号)控件或删除按钮时
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath;

函数描述请求数据源提交在调用方中插入或删除指定行的操作

当用户点击与表视图中的UITableViewCell对象相关联的插入(绿色加号)控件或删除按钮时,UITableView将此消息发送给数据源,要求它提交更改。(如果用户点击删除(红色-)控件,那么UITableView将显示Delete按钮以获得确认。)

数据源通过调用UITableView方法insertRowsAtIndexPaths:withRowAnimation:或deleteRowsAtIndexPaths:withRowAnimation:来提交插入或删除操作。要启用UITableView的“滑动删除”功能(用户水平地滑动一行来显示“删除”按钮),必须实现此方法。

不应该在这个方法的实现中调用setEditing:animated:。如果出于某种原因必须调用它,那么在使用performSelector:withObject:afterDelay:方法延迟之后调用它。

参数 :

tableView : 请求插入或删除的表视图对象。

editingStyle : 与indexPath指定的行所请求的插入或删除相对应的单元格编辑样式。可能的编辑样式是UITableViewCellEditingStyleInsert或UITableViewCellEditingStyleDelete。

indexPath : 在tableView中定位行的索引路径。

  • UITableViewCellEditingStyle枚举值如下:
typedef NS_ENUM(NSInteger, UITableViewCellEditingStyle) {
    UITableViewCellEditingStyleNone, //单元格没有编辑控件。这是默认值。
    UITableViewCellEditingStyleDelete, //单元格具有删除编辑控件;这个控件是一个包含负号的红色圆圈。
    UITableViewCellEditingStyleInsert  //单元格具有插入编辑控件;这个控件是一个绿色的圆圈,里面有一个加号。
};
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath;

函数描述通知数据源将UITableView中特定行的单元格移动到另一个位置。当用户按下fromRow中的reorder控件时,UITableView对象将此消息发送到数据源。

参数 :

tableView : 请求此操作的表视图对象。

sourceIndexPath : 在tableView中定位要移动的行的索引路径。

destinationIndexPath : 一个索引路径,用于定位作为移动目标的tableView中的行。

//UITableView右边索引栏的内容
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView{
}

函数描述 : 要求数据源返回表视图各组的标题作为右边索引栏的内容

参数 :

tableView : 请求此信息的表视图对象。

返回值 : 字符串数组,用作UITableView中节的标题并显示在UITableView右侧的索引列表中。例如对于按字母顺序排列的列表,可以返回一个包含字符串“A”到“Z”的数组。

UITableViewDelegate提供的一些方法
//选中了UITableView的某一行
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
}

函数描述通知UITableView选中了某一行的单元格,代理在此方法中处理单元格的选择。当UITableView的editing属性设置为“YES”(即表视图处于编辑模式)时,不会调用此方法。

参数 :

tableView : 通知代理有关新的某行单元格选择的表视图对象。

indexPath :在tableView中定位新选定的某行单元格的索引路径。

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(3.0));

函数描述通知代理指定行的单元格现在已经取消选择,代理在此方法中处理单元格的取消选择。

参数 :

tableView :一个表格视图,通知代理相关行单元格取消选择的信息。

indexPath:在tableView中定位被取消选择的某行单元格的索引路径。

//某一行单元格的高度
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
}

函数描述要求代理指定某一行单元格使用的高度。当UITableView中的每行单元格高度不完全相同时,重写此方法,如果每行单元格的高度相同,请不要重写此方法,而是为UITableView的rowHeight属性指定一个值。此方法返回的值优先于rowHeight属性中的值。

在单元格出现在屏幕上之前,UITableView为当前可见部分中的单元格调用此方法。当用户滚动时,UITableView仅在单元格在屏幕上移动时调用该方法,每次单元格出现在屏幕上时,它都调用该方法,而不管它以前是否在屏幕上出现过。

参数 :

tableView : 请求此信息的表视图对象。

indexPath : 在tableView中定位行的索引路径。

返回值 : 一个非负浮点值,指定一行单元格的高度(以点为单位)。

//某一节头部(页眉)视图的高度
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
}

函数描述向代理询问用于特定节头部(页眉)视图的高度。使用此方法指定tableView:viewForHeaderInSection:函数返回的自定义节头部(页眉)视图的高度。

参数 :

tableView : 请求此信息的表视图对象。

section :标识tableView的节的索引号。

返回值 :一个非负浮点值,指定节头部头部(页眉)视图高度(以点为单位)。

//某一节尾部(页脚)视图的高度
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{
}

函数描述 :向代理询问用于特定节尾部(页脚)视图的高度。使用此方法可以指定tableView:viewForFooterInSection:函数返回的自定义尾部(页脚)视图的高度。

参数 :

tableView : 请求此信息的表视图对象。

section :标识tableView的节的索引号。

返回值 :指定节的尾部(页脚)视图高度(以点为单位)的非负浮点值。

//某节头部显示的视图
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
}

函数描述 :要求代理在UITableView的指定节的头部标题中显示视图对象。使用此方法返回标题的UILabel、UIImageView或自定义视图,如果实现此方法还必须实现tableView: heightForHeaderInSection:函数以指定自定义视图的高度。

参数 :

tableView :请求视图对象的表视图对象。

section :包含头部视图的节的索引号。

返回值 : 要显示在指定节头部的视图对象

\color{red}{例如:头部某节头部显示一个标签}

- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    if(section == 0){
        //初始化要显示的标签富文本
        NSMutableAttributedString *newAttributedString = [[NSMutableAttributedString alloc]initWithString:@"所有信息保密,仅用于处方药审核"];
        //创建Image的富文本格式
        NSTextAttachment *attach = [[NSTextAttachment alloc] init];
        //这个-2.5是为了调整下标签跟文字的位置
        attach.bounds = CGRectMake(0, -2.5, 16, 16);
        //设置图片
        attach.image = [UIImage imageNamed:@"dun_pai"];
        //添加到富文本对象里
        NSAttributedString * imageStr = [NSAttributedString attributedStringWithAttachment:attach];
        //加入文字前面
        [newAttributedString insertAttributedString:imageStr atIndex:0];
        //初始化要显示的标签
        UILabel *titleLabel = [[UILabel alloc]initWithFrame:CGRectZero];
        //设置文本字体
        titleLabel.font = [YSCUiUtils fontFour];
        //设置文本颜色
        titleLabel.textColor = [YSCUiUtils colorThree];
        //设置文本对齐方式
        titleLabel.textAlignment = NSTextAlignmentCenter;
        //设置富文本
        titleLabel.attributedText = newAttributedString;
        //返回标签
        return titleLabel;
    }
    return nil;
}

-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
    if(section == 0){
        return 40;
    }
    return 10;
}

样式如图 :

截屏2020-08-18上午10.31.10.png
//某节尾部显示的视图
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{
}

函数描述 : 要求代理在UITableView的指定节的尾部标题中显示视图对象。使用此方法返回页脚的UILabel、UIImageView或自定义视图,如果实现此方法还必须实现tableView: heightForFooterInSection:函数以指定自定义视图的高度。

参数 :

tableView :请求视图对象的表视图对象。

section :包含尾部视图的分组的索引号。

返回值 :要显示在指定节尾部的视图对象。

//设置每一行的等级缩进
-(NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath{ 
}

函数描述要求代理返回给定节中某行系统提供的单元格标题文本缩进级别。对系统提供的单元格样式中的textLabel标签生效,对自定义单元格无效。

参数 :

tableView : 请求此信息的表视图对象。

indexPath : 在tableView中定位行的索引路径。

返回值 : 返回指定行的单元格标题文本缩进级别,负值无效,值越大,标题文本缩进越大。

如图 :

截屏2022-09-06 10.43.27.png
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;

函数描述通知代理UITableView将要为特定的行绘制单元格。在绘制一行单元格之前,UITableView将此消息发送给它的代理,从而允许代理在显示单元格对象之前自定义该对象。此方法使代理有机会重写表视图先前设置的基于状态的属性,例如选择时的颜色和背景色。代理返回后,表视图仅设置alpha和frame属性,然后仅设置行滑入或滑出时的动画。

参数 :

tableView : 通知代理即将发生的事件的表视图对象。

cell : tableView在绘制行时要使用的表视图单元格对象。

indexPath : 在tableView中定位特定行单元格的索引路径。

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath API_AVAILABLE(ios(6.0));

函数描述通知代理指定的单元格已从表中删除。使用此方法可以检测何时从表视图中删除单元格,而不是监视单元格本身以查看它何时出现或消失。

参数 :

tableView : 删除视图的表视图对象。

cell :被移除的单元格。

indexPath : 单元格的索引路径。

- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));

函数描述 :通知代理表视图将要绘制指定节的头部(页眉)视图

参数 :

tableView : 通知代理此事件的表视图对象。

view : 即将显示的头部(页眉)视图。

section : 包含头部(页眉)视图的节的索引号。

- (void)tableView:(UITableView *)tableView didEndDisplayingHeaderView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));

函数描述通知代理指定的节的头部(页眉)视图已从UITableView中删除。使用此方法检测从UITableView中删除头部(页眉)视图时的情况,而不是监视头部(页眉)视图本身,以查看它何时出现或消失。

参数 :

tableView : 删除视图的表视图对象。

view : 已删除的头部(页眉)视图。

section : 包含头部(页眉)视图的节的索引。

- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));

函数描述 : 通知代理表视图将要绘制指定节的尾部(页脚)视图

参数 :

tableView : 通知代理此事件的表视图对象。

view : 即将显示的尾部(页脚)视图。

section : 包含尾部(页脚)视图的节的索引号。

- (void)tableView:(UITableView *)tableView didEndDisplayingFooterView:(UIView *)view forSection:(NSInteger)section API_AVAILABLE(ios(6.0));

函数描述通知代理指定的节的尾部(页脚)视图已从UITableView中删除。使用此方法检测从UITableView中删除尾部(页脚)视图时的情况,而不是监视尾部(页脚)视图本身,以查看它何时出现或消失。

参数 :

tableView : 删除视图的表视图对象。

view : 已删除的尾部(页脚)视图。

section : 包含尾部(页脚)视图的节的索引。

UITableViewCell - 单元格

UITableView的每一行单元格都是一个UITableViewCell,在数据源(UITableViewDataSource)的tableView:cellForRowIndexPath:方法中来初始化每一行单元格。

UITableViewCell是UIView的子类,内部有个默认的子视图contentView。contentView是UITableViewCell所显示内容的父视图,并负责显示一些辅助指示视图。辅助指示视图是显示一个表示动作的图标,可以通过设置UITableViewCell的accessoryType来显示,默认是UITableViewCellAccessoryNone(不显示辅助指示图)。

  • 系统提供的其他辅助指示视图样式值如下:
屏幕快照 2019-03-03 下午11.41.04.png
UITableViewCell的contentView

contentView下默认有三个子视图(系统提供),其中的2个是UILabel(通过UITableViewCell的textLabel和detailTextLabel属性访问),第三个是UIImageView(通过UITableViewCell的imageView属性访问)。

UITableViewCell还有一个UITableViewStyle属性,用于决定使用contentView的哪些子视图,以及这些子视图在contentView中的位置,

  • UITableViewCell系统提供的contentView样式如下:
屏幕快照 2019-03-04 下午8.00.26.png
UITableViewCell自定义

当系统提供的UITableViewCell样式不能满足需求时,我们可以创建UITableViewCell类并继承UITableViewCell来自定义样式,重写UITableViewCell的初始化方法,例如:

-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if(self){
        self.backgroundColor = [UIColor whiteColor];
        [self drawCell];
    }
    return self;
}
UITableViewCell对象的重用原理

ios设备的内存有限,如果用UITableView显示成千上万条数据,就需要成千上万个UITableViewCell对象,那样的话将会耗尽ios设备的内存。要解决该问题,需要从用UITableViewCell对象。

重用原理:当滚动列表时,部分UITableViewCell会移除窗口,UITableView会将窗口外的UITableViewCell放入一个对象池中,等待重用。当UITableView要求dataSource返回UITableViewCell时,dataSource会先查看这个对象池,如果池中有未使用的UITableViewCell,dataSource会用新的数据配置这个UITableViewCell,然后返回给UITableView,重新显示到窗口中,从而避免创建新对象

还有一个非常重要的问题(cell复用错误问题):有时需要自定义UITableViewCell(用一个子类继承UITableViewCell),而且每一行用的不一定是同一种UITableViewCell(如微信聊天的布局),所以一个UITableView可能拥有不同类型的UITableViewCell,对象池中也会有很多不同类型的UITableViewCell,那么UITableView在重用UITableViewCell时可能会得到错误类型的UITableViewCell。

cell复用错误问题解决方案:UITableViewCell有个NSString *reuseIdentifier属性,可以在初始化UITableViewCell的时候传入一个特定的字符串标识来设定reuseIdentifier(一般用UITableViewCell的类名)。当UITableView要求dataSource返回UITableViewCell时,先通过一个字符串标识到对象池中查找对应类型的UITableViewCell对象,如果有,就重用,如果没有,就传入这个字符串标识来初始化一个UITableViewCell对象。

\color{red}{重用UITableViewCell对象的代码片段:}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    static NSString *identifier = @"UITableViewCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if(cell == nil){
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"Text %li",(long)indexPath.row];
    return cell;
}

\color{red}{不重用UITableViewCell对象的代码片段(不推荐):}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    static NSString *identifier = @"UITableViewCell";
    UITableViewCell *cell =[tableView cellForRowAtIndexPath:indexPath];
    if(cell == nil){
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"Text %li",(long)indexPath.row];
    return cell;
}
UITableViewCell的常用属性
@property (nonatomic, strong, nullable) UIView *backgroundView;

属性描述用作单元格背景的视图。对于普通样式表(UITableViewStylePlain)中的单元格,默认值为nil;对于分组样式表UITableViewStyleGrouped,默认值为非nil。UITableViewCell将背景视图添加为所有其他视图后面的子视图,并使用其当前frame位置。

@property (nonatomic, strong, nullable) UIView *selectedBackgroundView;

属性描述选定单元格时用作其背景的视图。对于普通样式表(UITableViewStylePlain)中的单元格,默认值为nil;对于节组表UITableViewStyleGrouped,默认值为非nil。UITableViewCell仅在选定单元格时才将此属性的值添加为子视图。它将选定的背景视图作为子视图添加到背景视图(backgroundView)正上方(如果backgroundView不是nil)或所有其他视图的后面。调用setSelected:animated:使选定的背景视图以alpha淡入和淡出动画。

@property (nonatomic) UITableViewCellSelectionStyle   selectionStyle;      

属性描述 : 单元格的选择样式。选择样式是backgroundView常量,用于确定选定单元格时单元格的颜色。

  • UITableViewCellSelectionStyle枚举值如下:
typedef NS_ENUM(NSInteger, UITableViewCellSelectionStyle) {
    UITableViewCellSelectionStyleNone,  //没有颜色   
    UITableViewCellSelectionStyleBlue,  //蓝色 
    UITableViewCellSelectionStyleGray,  //灰色  
    UITableViewCellSelectionStyleDefault API_AVAILABLE(ios(7.0))  //默认值
};
UITableViewCell的常用函数
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier API_AVAILABLE(ios(3.0)) NS_DESIGNATED_INITIALIZER;

函数描述使用样式和重用标识符初始化UITableView的格单元并将其返回给调用方。此方法是UITableViewCell类的指定初始化方法。

参数 :

style : 表示单元格样式的常量。关常量的描述可以查看UITableViewCellStyle。

reuseIdentifier :用于标识单元格对象的字符串,如果要重用该单元格对象来绘制表视图的多行单元格则需要传入正确的标识符。如果单元格对象不能被重用,则传递nil。对于相同形式的所有单元格,应该使用相同的重用标识符。

返回值 :一个已初始化的UITableViewCell对象,如果无法创建该对象,则为 nil。

- (void)prepareForReuse NS_REQUIRES_SUPER;   

函数描述如果UITableViewCell对象具有重用标识符,则UITableView会在表视图对象调用dequeueReusableCellWithIdentifier: 方法返回对象之前调用此方法。 为避免潜在的性能问题,应该只重置与内容无关的单元格属性,例如:alpha、编辑和选择状态。 重用单元格并重置所有内容时,应始终在UITableView 数据源的tableView:cellForRowAtIndexPath: 方法中进行。

如果单元格对象没有关联的重用标识符,或者如果您使用 reconfigureRowsAtIndexPaths: 更新现有单元格的内容,则表格视图不会调用此方法。如果重写此方法,则必须确保调用超类实现。

UITableViewController - 表视图控制器

UITableViewController是UIViewController的子类,UITableViewController的View是个UITableView,由UITableViewController负责设置和显示这个UITableView对象。UITableViewController对象被创建后,会将这个UITableView对象的dataSource和delegate指向UITableViewController自己。即UITableViewController默认扮演了三种角色:视图控制器,UITableView的数据源和代理。使用UITableViewController可以便捷的创建一个拥有表视图的控制器。

练习代码

\color{red}{设置UITableView中UITableViewCell的宽度:}

设置UITableViewCell的宽度需要在自定义的Cell中重写父类的setFrame:(CGRect)frame方法,例如Cell的宽度左右缩进10:

- (void)setFrame:(CGRect)frame {
    frame.origin.x += 10;

    frame.size.width -= 2 * 10;

    [super setFrame:frame];
}

效果大概是这样:

Jietu20191126-094847@2x.gif

\color{red}{通过模型控制表示图显示内容的长篇练习:}

不带选择按钮的cell

//
//  MessageListItemCell.h


#import <UIKit/UIKit.h>
#import "MessageItemModel.h"

UIKIT_EXTERN NSString * const MessageListItemCellReuseIdentifier;

@interface MessageListItemCell : UITableViewCell

@property (nonatomic, strong) UIView *readIdentifierView;//未读消息标记视图
@property (nonatomic, strong) UIImageView *customImageView;//消息图标视图
@property (nonatomic, strong) UILabel *titleLabel;//消息标题标签
@property (nonatomic, strong) UILabel *contentLabel;//消息内容标签
@property (nonatomic, strong) UILabel *dateLabel;//日期标签

@property (nonatomic, strong)MessageItemModel *mdoel;//模型

@end

//
//  MessageListItemCell.m


#import "MessageListItemCell.h"

NSString * const MessageListItemCellReuseIdentifier = @"MessageListItemCellReuseIdentifier";
static CGSize const ReadIdentifierViewSize = {10.0, 10.0};

@interface MessageListItemCell()

@property (nonatomic, strong) CALayer *separatorLineLayer;//底部分割线

@end


@implementation MessageListItemCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if(self){
        self.selectionStyle = UITableViewCellSelectionStyleNone;
        [self createUI];
    }
    return self;
}

- (void)createUI{
    ///消息图标视图
    self.customImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
    [self.contentView addSubview:self.customImageView];
    
    ///未读消息标记视图
    self.readIdentifierView = [[UIView alloc] initWithFrame:CGRectZero];
    [self.contentView addSubview:self.readIdentifierView];
    self.readIdentifierView.backgroundColor = [UIColor greenColor];
    self.readIdentifierView.layer.cornerRadius = ReadIdentifierViewSize.height * 0.5;
    self.readIdentifierView.layer.masksToBounds = YES;
    
    ///消息标题标签
    self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    [self.contentView addSubview:self.titleLabel];
    self.titleLabel.font = [UIFont systemFontOfSize:16];
    self.titleLabel.textColor = [UIColor blackColor];
    
    ///消息内容标签
    self.contentLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    [self.contentView addSubview:self.contentLabel];
    self.contentLabel.font = [UIFont systemFontOfSize:13];
    self.contentLabel.textColor = [UIColor grayColor];
    
    ///日期标签
    _dateLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    [self.contentView addSubview:_dateLabel];
    _dateLabel.font = [UIFont systemFontOfSize:11];
    _dateLabel.textColor = [UIColor grayColor];
    
    ///底部分割线
    self.separatorLineLayer = [[CALayer alloc] init];
    [self.contentView.layer addSublayer:_separatorLineLayer];
    self.separatorLineLayer.backgroundColor = [UIColor grayColor].CGColor;
    
    [self setupConstraints];
    
}

///设置布局
- (void)setupConstraints {
    
    ///消息图标视图
    [self.customImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.contentView);
        make.left.equalTo(self.contentView).offset(10);
        make.size.mas_equalTo(CGSizeMake(54.0, 54.0));
    }];
    
    ///未读消息标记视图
    [self.readIdentifierView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.customImageView.mas_top).offset(10 * 0.5);
        make.right.equalTo(self.customImageView.mas_right).offset(-10 * 0.5);
        make.size.mas_equalTo(ReadIdentifierViewSize);
    }];
    
    ///日期标签
    [self.dateLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.top.equalTo(self.contentView).offset(10);
        make.right.equalTo(self.contentView).offset(-10);
    }];
    
    ///消息标题标签
    [self.titleLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.top.equalTo(self.contentView).offset(10);
        make.left.equalTo(self.customImageView.mas_right).offset(10);
        make.right.lessThanOrEqualTo(self.contentView).offset(-10);
    }];
    
    ///消息内容标签
    [self.contentLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.left.equalTo(self.titleLabel);
        make.top.equalTo(self.titleLabel.mas_bottom).offset(8);
        make.right.lessThanOrEqualTo(self.contentView).offset(-10);
        make.bottom.equalTo(self.contentView).offset(-10);
    }];

}

///底部分割线
- (void)layoutSubviews {
    [super layoutSubviews];
    self.separatorLineLayer.frame = CGRectMake(59, CGRectGetHeight(self.contentView.frame) - 0.5, CGRectGetWidth(self.contentView.frame), 0.6);
}

///格式化时间戳
- (NSString *)formatDate:(NSString *)timeStr{
    
    //返回当前时间的时间戳
    double timeStamp = [timeStr doubleValue];
    //NSTimeInterval 时间间隔,double类型
    NSTimeInterval time = timeStamp;
    //得到Date类型的时间,这个时间是1970-1-1 00:00:00经过你时间戳的秒数之后的时间
    NSDate * detaildate = [NSDate dateWithTimeIntervalSince1970:time];
    //实例化NSDateFormatter对象
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
    //设定时间格式,这里可以设置成自己需要的格式
    [dateFormatter setDateFormat:@"yyyy-MM-dd"];
    //转换成字符串
    NSString * currentDateStr = [dateFormatter stringFromDate:detaildate];
    return currentDateStr;
    
}

///设置模型
- (void)setMdoel:(MessageItemModel *)mdoel{
    self.titleLabel.text = mdoel.title;
    self.contentLabel.text = mdoel.content;
    self.dateLabel.text = [self formatDate:mdoel.send_time];
    self.customImageView.image = [UIImage imageNamed:@"bg_message_notice"];
}

- (void)awakeFromNib {
    [super awakeFromNib];
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];
}

@end

带选择按钮的cell

//
//  MessageListItemEditCell.h

#import "MessageListItemCell.h"

UIKIT_EXTERN NSString *const MessageListItemEditCellReuseIdentifier;

@interface MessageListItemEditCell : MessageListItemCell


@end

//
//  MessageListItemEditCell.m


#import "MessageListItemEditCell.h"

NSString *const MessageListItemEditCellReuseIdentifier = @"MessageListItemEditCellReuseIdentifier";

static CGSize const ReadIdentifierViewSize = {10.0, 10.0};

@interface MessageListItemEditCell()

@property (nonatomic, strong) UIButton *checkoutButton;

@end


@implementation MessageListItemEditCell

- (void)setupConstraints {
    
    ///选中按钮
    [self.checkoutButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.contentView).offset(10);
        make.centerY.equalTo(self.contentView);
        make.size.mas_equalTo(CGSizeMake(22, 22));
    }];
    
    ///消息图标视图
    [self.customImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.contentView);
        make.left.equalTo(self.checkoutButton.mas_right).offset(10);
        make.size.mas_equalTo(CGSizeMake(54.0, 54.0));
    }];
    
    ///未读消息标记视图
    [self.readIdentifierView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.customImageView.mas_top).offset(10 * 0.5);
        make.right.equalTo(self.customImageView.mas_right).offset(-10 * 0.5);
        make.size.mas_equalTo(ReadIdentifierViewSize);
    }];
    
    ///日期标签
    [self.dateLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.top.equalTo(self.contentView).offset(10);
        make.right.equalTo(self.contentView).offset(-10);
    }];
    
    ///消息标题标签
    [self.titleLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.top.equalTo(self.contentView).offset(10);
        make.left.equalTo(self.customImageView.mas_right).offset(10);
        make.right.lessThanOrEqualTo(self.contentView).offset(-10);
    }];
    
    ///消息内容标签
    [self.contentLabel mas_makeConstraints:^(MASConstraintMaker* make) {
        make.left.equalTo(self.titleLabel);
        make.top.equalTo(self.titleLabel.mas_bottom).offset(8);
        make.right.lessThanOrEqualTo(self.contentView).offset(-10);
        make.bottom.equalTo(self.contentView).offset(-10);
    }];
    
}

///懒加载选中按钮
- (UIButton *)checkoutButton {
    if (_checkoutButton == nil) {
        _checkoutButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [self.contentView addSubview:_checkoutButton];
        _checkoutButton.userInteractionEnabled = NO;
        [_checkoutButton setImage:[UIImage imageNamed:@"bg_check_normal"] forState:UIControlStateNormal];
        [_checkoutButton setImage:[UIImage imageNamed:@"bg_check_selected"] forState:UIControlStateSelected];
    }
    return _checkoutButton;
}

///设置模型
- (void)setMdoel:(MessageItemModel *)model{
    [super setMdoel:model];
    //设置按钮的选中状态
    self.checkoutButton.selected = model.selected;
}

- (void)awakeFromNib {
    [super awakeFromNib];
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

}

@end

操作的模型

//
//  MessageItemModel.h


#import <Foundation/Foundation.h>


@interface MessageItemModel : NSObject

@property (nonatomic, copy) NSString *send_time;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *content;
@property (nonatomic, assign) BOOL selected;

- (instancetype)initWithModel;

@end

//
//  MessageItemModel.m


#import "MessageItemModel.h"

@implementation MessageItemModel

- (instancetype)initWithModel{
    
    self = [super init];
    if(self){
        self.send_time = @"1576139886";
        self.content = @"亲爱的顾客小懒,您的账户有余额变动,变动资金-103.40元,请您及时查看账户资金明细。";
        self.title = @"余额变动提醒";
        self.selected = NO;
    }
    
    return self;
}

@end

控制器

//
//  TestTableViewDetelateCellController.h


#import <UIKit/UIKit.h>

@interface TestTableViewDetelateCellController : UIViewController

@end

//
//  TestTableViewDetelateCellController.m


#import "TestTableViewDetelateCellController.h"
#import "MessageListItemCell.h"
#import "MessageListItemEditCell.h"
#import "MessageItemModel.h"

///管理视图
@interface MessageManagementView : UIView

@property (nonatomic, strong) UIButton *checkButton;//全部选中按钮
@property (nonatomic, strong) UIButton *deleteButton;//删除按钮
@property (nonatomic, strong) NSString *selectedCount;//选中的消息条数
@property (nonatomic, strong) CALayer *separatorLineLayer;//顶部分割线

@end

@implementation MessageManagementView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        
        self.backgroundColor = [UIColor whiteColor];
        
        ///顶部分割线
        _separatorLineLayer = [[CALayer alloc] init];
        [self.layer addSublayer:_separatorLineLayer];
        _separatorLineLayer.backgroundColor = [UIColor blackColor].CGColor;
        
        ///全部选中按钮
        _checkButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [self addSubview:_checkButton];
        _checkButton.titleLabel.font = [UIFont systemFontOfSize:15];
        _checkButton.titleEdgeInsets = UIEdgeInsetsMake(0, 5.0, 0, -5.0);
        [_checkButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
        [_checkButton setImage:[UIImage imageNamed:@"bg_check_normal"] forState:UIControlStateNormal];
        [_checkButton setImage:[UIImage imageNamed:@"bg_check_selected"]forState:UIControlStateSelected];
        
        ///删除按钮
        _deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
        [self addSubview:_deleteButton];
        _deleteButton.backgroundColor = [UIColor redColor];
        _deleteButton.titleLabel.font = [UIFont systemFontOfSize:15];
        [_deleteButton setTitle:@"删除" forState:UIControlStateNormal];
        [_deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];

        [self setupConstraints];
    }
    return self;
}

- (void)setupConstraints {
    ///全部选中按钮
    [_checkButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self).offset(10);
        make.centerY.equalTo(self.deleteButton);
    }];
    
    ///删除按钮
    [_deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.right.equalTo(self);
        make.height.mas_equalTo(50);
        make.width.mas_equalTo(50 * 2.0);
    }];
}

///布局子视图
- (void)layoutSubviews {
    [super layoutSubviews];
    _separatorLineLayer.frame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), 0.5);
}

///设置选择了多少条消息
- (void)setSelectedCount:(NSString *)selectedCount {
    _selectedCount = [selectedCount copy];
    if (_selectedCount == nil) {
        _selectedCount = @"0";
    }

    NSString *selectedCountFormat = nil;
    selectedCountFormat = [NSString stringWithFormat:@"已选 %@ 条消息", _selectedCount];
    NSMutableAttributedString *selectedAttrCountFormat = [[NSMutableAttributedString alloc] initWithString:selectedCountFormat];
    //设置按钮标题为黑色
    [selectedAttrCountFormat setAttributes:@{NSForegroundColorAttributeName: [UIColor blackColor]} range:NSMakeRange(0, selectedCountFormat.length)];
    //设置消息条数为k红色
    [selectedAttrCountFormat setAttributes:@{NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(3, _selectedCount.length)];
    //设置按钮标题
    [_checkButton setAttributedTitle:selectedAttrCountFormat forState:UIControlStateNormal];
}


@end



@interface TestTableViewDetelateCellController()<UITableViewDataSource,UITableViewDelegate>

@property (nonatomic, strong) NSMutableArray *dataSource;//数据源
@property (nonatomic) BOOL editMode;//编辑模式
@property (nonatomic, strong) MessageManagementView *managementView;//消息管理视图
@property (nonatomic, strong) UITableView *tableView;//表视图

@end

@implementation TestTableViewDetelateCellController

- (void)viewDidLoad{
    
    [super viewDidLoad];
    self.navigationItem.title = @"消息盒子";
    [self initDataSource];
    [self setupNavigationBarButtonItem];
    [self setupTableView];
    [self setMessageManagementView];
}

///初始化导航栏右侧管理按钮
- (void)setupNavigationBarButtonItem {
    
    NSDictionary *textAttribute = @{NSFontAttributeName: [UIFont systemFontOfSize:15],
                                    NSForegroundColorAttributeName: [UIColor grayColor]};
    
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"管理" style:UIBarButtonItemStylePlain target:self action:@selector(editCollections:)];
    [self.navigationItem.rightBarButtonItem setTitleTextAttributes:textAttribute forState:UIControlStateNormal];
    [self.navigationItem.rightBarButtonItem setTitleTextAttributes:textAttribute forState:UIControlStateHighlighted];
    [self.navigationItem.rightBarButtonItem setTitleTextAttributes:textAttribute forState:UIControlStateDisabled];
}

///管理按钮点击事件
- (void)editCollections:(UIBarButtonItem *)sender {
    
    if ([sender.title isEqualToString:@"管理"]) {
        sender.title = @"完成";
        self.editMode = YES;
    } else {
        sender.title = @"管理";
        self.editMode = NO;
    }
    NSInteger count = 0;
    for (MessageItemModel *model in self.dataSource) {
        //如果模型中有一条消息处于未选中的状态
        if (model.selected == YES) {
            //累加选中消息的条数
            count++;
        }
    }
    //设置消息管理视图选中的消息条数
    self.managementView.selectedCount = [NSString stringWithFormat:@"%zd", (size_t)count];
}

///初始化表视图
- (void)setupTableView {
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    [self.view addSubview:self.tableView];
    self.tableView.backgroundColor = [UIColor grayColor];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    self.tableView.estimatedRowHeight = 70.0;
    [self.tableView registerClass:[MessageListItemCell class] forCellReuseIdentifier:MessageListItemCellReuseIdentifier];
    [self.tableView registerClass:[MessageListItemEditCell class] forCellReuseIdentifier:MessageListItemEditCellReuseIdentifier];
    
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}


///设置消息管理视图
- (void)setMessageManagementView {
    
    self.managementView = [[MessageManagementView alloc] initWithFrame:CGRectZero];
    self.managementView.selectedCount = 0;
    self.managementView.hidden = YES;
    [self.view addSubview:self.managementView];
     [self.managementView.checkButton addTarget:self action:@selector(selectAll:) forControlEvents:UIControlEventTouchUpInside];
    [self.managementView.deleteButton addTarget:self action:@selector(deleteMessage) forControlEvents:UIControlEventTouchUpInside];
}

///消息管理视图选中按钮的点击事件
- (void)selectAll:(UIButton *)sender {
    //对选中状态取反
    sender.selected = !sender.selected;
    if (sender.selected) {
        //是选中状态,设置选中的消息条数作为消息管理视图的选中按钮的标题
        self.managementView.selectedCount = [NSString stringWithFormat:@"%zd", (size_t)self.dataSource.count];
    } else {
        //不是选中状态,设置选中的消息条数为0作为消息管理视图的选中按钮的标题
        self.managementView.selectedCount = @"0";
    }
    
    //修改数据源中模型所有的是否选中的状态
    for (MessageItemModel *model in self.dataSource) {
        model.selected = sender.selected;
    }
    //刷新表视图
    [self.tableView reloadData];
}

///消息管理视图删除按钮的点击事件
- (void)deleteMessage{
    //倒序遍历数据源,防止由于删除元素时索引的变化,会造成删除错误
    [self.dataSource enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(MessageItemModel *model, NSUInteger idx, BOOL * _Nonnull stop) {
        //如果模型中的是否选中的状态是选中的,点击删除按钮时将其移出数据源
        if(model.selected){
            [self.dataSource removeObject:model];
        }
    }];
    //设置选中的消息条数为0作为消息管理视图的选中按钮的标题
    self.managementView.selectedCount = @"0";
    //刷新表视图
    [self.tableView reloadData];
    
}

///编辑模式改变时执行的方法(管理按钮管理与完成切换时)
- (void)setEditMode:(BOOL)editMode {
    _editMode = editMode;
    
    if (self.dataSource.count == 0) {
        return;
    }
    if (_editMode) {
        //点击管理按钮时
        self.managementView.hidden = NO;
        [self.managementView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.left.bottom.right.equalTo(self.view);
            make.height.mas_equalTo(50 + [self bottomPadding]);
        }];
        
        [self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.top.left.right.equalTo(self.view);
            make.bottom.equalTo(self.managementView.mas_top);
        }];
        
    } else {
        //点击完成按钮时
        self.managementView.hidden = YES;
        
        [self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.edges.equalTo(self.view);
        }];
    }
    //设置消息管理视图选择按钮位未选中状态
    self.managementView.checkButton.selected = NO;
    //刷新表视图
    [self.tableView reloadData];
}

///初始化数据源
- (void)initDataSource{
    self.dataSource = [[NSMutableArray alloc]init];
    for (int i = 0; i < 15; i++) {
        MessageItemModel *model = [[MessageItemModel alloc]initWithModel];
        [self.dataSource addObject: model];
    }
}

///返回底部安全区高度
- (CGFloat)bottomPadding{
    
    if (DEVICE_IS_IPHONE_X) {
        return 34.0;
    } else {
        return 0;
    }
}

///消息管理视图的选择按钮是否要处于相中状态
- (BOOL)checkSelectAll {
    //默认全部是选中的
    BOOL selectAll = YES;
    //继续选中的消息数量
    NSInteger count = 0;
    for (MessageItemModel *model in self.dataSource) {
        //如果模型中有一条消息处于未选中的状态
        if (model.selected == NO) {
            //修改全部选中的状态
            selectAll = NO;
        } else {
            //累加选中消息的条数
            count++;
        }
    }
    //设置消息管理视图选中的消息条数
    self.managementView.selectedCount = [NSString stringWithFormat:@"%zd", (size_t)count];
    //返回消息管理视图的选中按钮是否要选中
    return selectAll;
}


#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.dataSource.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    if (!self.editMode) {
        //未开始管理与管理完成状态的cell
        MessageListItemCell *cell = [tableView dequeueReusableCellWithIdentifier:MessageListItemCellReuseIdentifier];
        [cell setMdoel:self.dataSource[indexPath.row]];
        return cell;
    }else{
        //管理状态的cell
        MessageListItemEditCell *cell = [tableView dequeueReusableCellWithIdentifier:MessageListItemEditCellReuseIdentifier];
        [cell setMdoel:self.dataSource[indexPath.row]];
        return cell;
    }
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    
    if (self.editMode) {
        //进入管理状态时
        MessageItemModel *model = self.dataSource[indexPath.row];
        //对模型的选中状态进行取反
        model.selected = !model.selected;
        //刷新点击的的行
        [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        //消息管理视图的选择按钮是否要处于选中状态
        self.managementView.checkButton.selected = [self checkSelectAll];
    }
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{
    if(self.editMode){
        //进入管理状态时不允许单行删除
        return NO;
    }else{
        return YES;
    }
}

- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath{
    //滑动单行cell以删除
    UITableViewRowAction *deteleAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@"删除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
        [self.dataSource removeObjectAtIndex:indexPath.row];
        [self.tableView reloadData];
    }];
    deteleAction.backgroundColor = [UIColor blueColor];
    return @[deteleAction];
}


@end

用到的宏

#define DEVICE_IS_IPHONE_X ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125.0, 2436.0), [[UIScreen mainScreen] currentMode].size) : NO)

效果如下 :

Jietu20191215-231828.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容