UITableView操作拓展
在看过网课视频后,我们会得到这样的一段代码
objectivec
#import "SceneDelegate.h"
#import "ViewController.h"
@interface SceneDelegate ()
@end
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// 将传入的通用 UIScene 对象转换为具体的 UIWindowScene(因为窗口需要依附于场景)
UIWindowScene* windoeSence = (UIWindowScene*) scene;
// 使用 windowScene 创建应用程序的主窗口
self.window = [[UIWindow alloc] initWithWindowScene:windoeSence];
// 使窗口成为主窗口并显示(注意:这里调用了两次,第二次是多余的,但通常只需一次)
[self.window makeKeyAndVisible];
// 创建导航控制器,并将 ViewController 作为根视图控制器
UINavigationController* nav = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]];
// 将导航控制器设置为窗口的根视图控制器,决定窗口显示的内容
self.window.rootViewController = nav;
// 再次使窗口成为主窗口并显示(实际上一行代码已经设置了 rootViewController,这里重复调用无负面影响)
[self.window makeKeyAndVisible];
}
@end
objectivec
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
<UITableViewDataSource, UITableViewDelegate>
{
UITableView* _tableView; // 表格视图,用于显示数据列表
NSMutableArray* _arrayData; // 可变数组,存储表格每一行显示的字符串数据
UIBarButtonItem* _btnEdit; // 导航栏右侧的"编辑"按钮,点击后进入编辑模式
UIBarButtonItem* _btnFinish; // 导航栏右侧的"完成"按钮,点击后退出编辑模式
UIBarButtonItem* _btnDelete; // 导航栏左侧的"删除"按钮
BOOL _isEdit; // 记录当前是否处于编辑模式(YES 表示正在编辑,NO 表示普通模式)
}
@end
objectivec
// ViewController.m
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.tableHeaderView = nil;
_tableView.tableFooterView = nil;
[self.view addSubview:_tableView];
_arrayData = [[NSMutableArray alloc] init];
for (int i = 0; i < 20; i++) {
NSString* str = [NSString stringWithFormat:@"A %d", i];
[_arrayData addObject:str];
}
[_tableView reloadData];
[self creatBtn];
}
-(void) creatBtn {
_isEdit = NO;
_btnEdit = [[UIBarButtonItem alloc] initWithTitle:@"编辑" style:UIBarButtonItemStylePlain target:self action:@selector(pressEdit)];
_btnFinish = [[UIBarButtonItem alloc] initWithTitle:@"完成" style:UIBarButtonItemStylePlain target:self action:@selector(pressFinish)];
_btnDelete = [[UIBarButtonItem alloc] initWithTitle:@"删除" style:UIBarButtonItemStylePlain target:self action:@selector(pressDelete)];
self.navigationItem.rightBarButtonItem = _btnEdit;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"Delete");
[_arrayData removeObjectAtIndex:indexPath.row];
[_tableView reloadData];
}
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"选中单元格 %d, %d", indexPath.section, indexPath.row);
}
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"取消选中单元格 %d, %d", indexPath.section, indexPath.row);
}
-(UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleDelete;
}
-(void) pressEdit {
_isEdit = YES;
self.navigationItem.rightBarButtonItem = _btnFinish;
[_tableView setEditing:YES];
self.navigationItem.leftBarButtonItem = _btnDelete;
}
-(void) pressFinish {
_isEdit = NO;
self.navigationItem.rightBarButtonItem = _btnEdit;
[_tableView setEditing:NO];
self.navigationItem.leftBarButtonItem = nil;
}
-(void) pressDelete {
}
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _arrayData.count;
}
//默认返回1
-(NSInteger) numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString* strID = @"ID";
UITableViewCell* cell = [_tableView dequeueReusableCellWithIdentifier:strID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:strID];
}
cell.textLabel.text = [_arrayData objectAtIndex:indexPath.row];
cell.detailTextLabel.text = @"子标题";
return cell;
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 60;
}
@end
运行这段代码可以让我们得到很多可以删除的单元格,但是在日常生活中我们可能会要多个选中批量操作,会想编辑其中的内容,等等需求,所以我们可以去学习这些方法美化我们的UI
头部视图视差效果
objectivec
// 创建一个头部视图(容器),用于放置视差图片,高度为 200,宽度与屏幕相同
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 200)];
// 开启父视图的裁剪功能,防止子视图(图片)放大时超出边界而覆盖下面的 cell
headerView.clipsToBounds = YES;
// 创建图片视图,加载名为 "1.jpg" 的图片
UIImageView *parallaxImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1.jpg"]];
// 设置图片填充模式为"等比例填充并填满视图",可能裁剪图片边缘,保证视图始终被填满
parallaxImageView.contentMode = UIViewContentModeScaleAspectFill;
// 裁剪图片视图自身超出边界的部分(与父视图的 clipsToBounds 配合)
parallaxImageView.clipsToBounds = YES;
// 设置图片视图的初始位置和大小与父视图(headerView)一致
parallaxImageView.frame = headerView.bounds;
// 将图片视图添加到头部容器视图中
[headerView addSubview:parallaxImageView];
// 将头部视图设置为表格的 tableHeaderView(显示在第一个 cell 之前)
_tableView.tableHeaderView = headerView;
// 保存图片视图的引用,以便在滚动代理中实现视差效果(缩放或移动)
self.parallaxImageView = parallaxImageView;
objectivec
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 获取当前表格视图的垂直滚动偏移量
CGFloat offsetY = scrollView.contentOffset.y;
// 计算导航栏背景的不透明度:滚动超过 80 点时完全不透明,否则按比例渐变
CGFloat alpha = MIN(1, offsetY / 80.0);
// 设置导航栏背景色为白色,透明度随着滚动变化(实现滚动渐变效果)
self.navigationController.navigationBar.backgroundColor = [UIColor colorWithWhite:1 alpha:alpha];
// 头部图片视差效果(下拉时放大图片)
if (offsetY < 0) {
// 下拉距离越大,缩放比例越大。公式:1 + (向下拉的距离)/100,最大限制为 2.0
CGFloat scale = MIN(2.0, 1 + (-offsetY) / 100);
// 对图片视图进行等比缩放,产生"拉近"的视觉效果
self.parallaxImageView.transform = CGAffineTransformMakeScale(scale, scale);
} else {
// 正常滚动或向上滚动时,恢复图片原始大小
self.parallaxImageView.transform = CGAffineTransformIdentity;
}
}
导航栏渐变:向上滚动时,offsetY 为正,alpha 从 0 逐渐增至 1,导航栏背景从透明变为白色。
图片下拉放大:当向下拉超出顶部(offsetY < 0)时,缩放比例随下拉距离增大,最大 2 倍;松手后恢复原状。
父视图 headerView 设置了 clipsToBounds = YES,图片放大后超出头部范围的部分被裁剪,不会覆盖下面 cell。
我们在表格的上方创建了一个视图用来放图片,在scrollViewDidScroll中先获取图片的纵轴偏移量,来决定导航栏什么时候变透明来展示图片,并决定在什么时候放大图片来实现景深效果
直接编辑单元格内容
在上面的代码中,每个单元格里面的内容都是固定的,我们可以通过专门创建单元格的类来自由编辑内容
objectivec
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// 自定义的可编辑单元格,用于在表格中直接编辑文本内容
@interface EditableCell : UITableViewCell
// 显示普通文本的标签(非编辑状态时显示)
@property (nonatomic, strong) UILabel* titleLabel;
// 编辑文本的输入框(编辑状态时显示)
@property (nonatomic, strong) UITextField* textField;
// 当文本编辑完成(退出编辑模式)时的回调 Block
// 参数 newText 为用户修改后的文本
@property (nonatomic, copy) void (^onTextChanged)(NSString* newText);
// @param editing YES 表示进入编辑模式,NO 表示退出编辑模式
// @param animated 是否使用动画效果
-(void) setEditing:(BOOL)editing animated:(BOOL)animated;
@end
NS_ASSUME_NONNULL_END
objectivec
#import "EditableCell.h"
@interface EditableCell ()
@end
@implementation EditableCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
// 调用父类的初始化方法
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// 创建显示文本的标签(普通状态下可见)
self.titleLabel = [[UILabel alloc] init];
// 创建文本输入框(编辑状态下可见)
self.textField = [[UITextField alloc] init];
// 设置输入框的边框样式为圆角矩形
self.textField.borderStyle = UITextBorderStyleRoundedRect;
// 初始时隐藏输入框,只显示标签
self.textField.hidden = YES;
// 将标签和输入框添加到 cell 的 contentView 上
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.textField];
}
return self;
}
// 当 cell 的布局需要更新时调用(例如设置 frame 或滚动时)
- (void)layoutSubviews {
[super layoutSubviews];
// 让标签和输入框的尺寸与 contentView 一致(填满整个 cell)
self.titleLabel.frame = self.contentView.bounds;
self.textField.frame = self.contentView.bounds;
}
// 重写 UITableViewCell 的 setEditing:animated: 方法,用于响应编辑模式的切换
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
// 调用父类方法,处理系统的编辑行为(例如出现删除按钮等)
[super setEditing:editing animated:animated];
// 根据编辑状态切换标签和输入框的可见性
// 编辑模式:隐藏标签,显示输入框
self.titleLabel.hidden = editing;
self.textField.hidden = !editing;
if (editing) {
// 进入编辑模式时,将当前标签的文本赋值给输入框,作为待编辑的内容
self.textField.text = self.titleLabel.text;
// 注意:这里没有调用 becomeFirstResponder,让用户手动点击输入框再弹出键盘
} else {
// 退出编辑模式时,将输入框中的新文本同步回标签
self.titleLabel.text = self.textField.text;
// 如果存在外部设置的 block 回调,则调用它,将修改后的文本传出
if (self.onTextChanged) {
self.onTextChanged(self.textField.text);
}
}
}
@end
objectivec
// UITableViewDataSource 协议方法:为表格的每一行提供并配置单元格
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 从复用池中获取可复用的自定义单元格(标识符为 "EditableCell")
EditableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"EditableCell" forIndexPath:indexPath];
// 设置单元格的标签文本为数据源中对应行的字符串
cell.titleLabel.text = _arrayData[indexPath.row];
// 设置一个回调 block,当用户编辑完成(退出编辑模式)时会执行此 block
// 参数 newText 是用户修改后的文本,我们需要将其更新回数据源
cell.onTextChanged = ^(NSString *newText) {
// 将修改后的文本重新保存到数据源数组的对应位置
_arrayData[indexPath.row] = newText;
};
// 根据当前视图控制器的编辑状态(_isEdit 标志),同步设置单元格的编辑模式
// animated:NO 表示不要动画,直接切换(因为外部已经通过导航栏按钮统一控制)
[cell setEditing:_isEdit animated:NO];
// 返回配置好的单元格
return cell;
}
数组和单元格
- 支持动态增删改
添加元素:addObject: → 插入新行
删除元素:removeObjectAtIndex: → 删除对应行
修改元素:直接替换数组中的对象 → 刷新对应行
操作数组的同时,同步调用表格的 insertRows/deleteRows/reloadRows,就能保证数据和界面一致。如果没有数组,这些操作将变得非常困难(例如需要维护一个复杂的数据结构或硬编码内容)。
- 知道行数和显示什么
UITableView 本身不持有数据,它通过 dataSource 协议向控制器"询问":
numberOfRowsInSection: → 有多少行?
cellForRowAtIndexPath: → 这一行显示什么内容?
如果没有一个独立的数据源(比如数组),控制器就无法回答这些问题。数组天然就是有序集合,非常适合按行索引(indexPath.row)来存储和提供数据。
- 支持复用机制
UITableView 的 cell 复用机制要求每次 cellForRowAtIndexPath: 被调用时,都要重新设置 cell 的内容(因为复用的 cell 可能残留旧数据)。因此必须有一个可靠的数据源,每次都能根据 indexPath 快速取出正确的数据。数组恰好提供了 O(1) 的随机访问能力,非常适合这种场景。
- 数据持久化
数组通常是临时存储,方便后续将数据存入 NSUserDefaults、文件、数据库(如 Core Data)等。如果数据散落在各处,持久化将极其复杂。
当然数据存入数组中还能批量删除,重排顺序之类的操作
自定义单元格EditableCell
EditableCell继承自UITableViewCell包含两个子视图:
titleLabel:普通状态下显示文本。
textField:编辑状态下显示,用于修改文本。
核心实现:setEditing,layoutSubviews
- 配置cell并绑定数据
tableView
复用:从复用池取出或创建 cell。
数据绑定:将当前行的数据赋给 titleLabel。
回调设置:当用户编辑完成(退出编辑模式)时,将新文本写回数据源。
编辑状态同步:根据全局 _isEdit 标志,调用 setEditing:animated: 使 cell 进入或退出编辑模式。
批量删除
objectivec
-(void) pressEdit {
_isEdit = YES;
self.navigationItem.rightBarButtonItem = _btnFinish;
[_tableView setEditing:YES animated:YES];
_tableView.allowsMultipleSelectionDuringEditing = YES; // 允许多选
self.navigationItem.leftBarButtonItem = _btnDelete; // 左侧只显示删除按钮
}
objectivec
-(void) pressFinish {
_isEdit = NO;
self.navigationItem.rightBarButtonItem = _btnEdit;
[_tableView setEditing:NO animated:YES];
self.navigationItem.leftBarButtonItem = nil;
}
objectivec
-(void) pressDelete {
// 获取当前在编辑模式下所有被选中的行的索引路径(多选圆圈勾选的行)
NSArray *selectedRows = [_tableView indexPathsForSelectedRows];
// 如果没有选中任何行,则弹出提示框,告知用户至少选择一项
if (selectedRows.count == 0) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:@"请至少选择一项" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
return;
}
// 将选中的索引路径按行号从大到小排序(降序),避免删除时因索引变化导致后续索引错误
NSArray *sorted = [selectedRows sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *a, NSIndexPath *b) {
return a.row > b.row ? NSOrderedDescending : NSOrderedAscending;
}];
// 删除数据源中对应的元素(从后往前删,保证索引有效)
for (NSIndexPath *ip in sorted) {
[_arrayData removeObjectAtIndex:ip.row];
}
// 从表格中删除对应的行(带动画效果)
[_tableView deleteRowsAtIndexPaths:selectedRows withRowAnimation:UITableViewRowAnimationAutomatic];
// 注意:此处不自动退出编辑模式,以便用户可继续选择其他行进行删除
}
编辑模式:UITableView 通过 setEditing:animated: 切换状态。进入编辑模式后,可以自定义cell的编辑样式(editingStyleForRowAtIndexPath:),并控制是否允许多选(allowsMultipleSelectionDuringEditing)。
多选机制:当 editingStyleForRowAtIndexPath: 返回 Delete 且 allowsMultipleSelectionDuringEditing = YES 时,系统会自动在每行左侧显示灰色圆圈,用于多选。用户勾选后,通过 indexPathsForSelectedRows 获取所有选中行。
批量删除原理:首先对选中的索引路径按行号降序排序,避免删除时索引错乱;然后从数据源(_arrayData)中移除对应元素;最后调用 deleteRowsAtIndexPaths:withRowAnimation: 刷新表格,保持视图与数据同步。
数据一致性:批量删除时,必须保证数据源修改和表格行删除在同一个逻辑块中执行,并且确保删除顺序正确(从大到小),否则会导致索引错位或崩溃。