iOS开发基础20-UITableView 动态更新:全局刷新、局部刷新、左滑操作与批量删除

UITableView 动态更新:全局刷新、局部刷新、左滑操作与批量删除

UITableView 的数据动态更新是开发中的常见需求。本文详细讲解全局刷新、局部刷新、左滑操作、编辑模式和批量删除的实现,并分析底层机制与性能优化。


一、前期准备

1. 数据模型

// XMGWine.h
@interface XMGWine : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *money;
@property (nonatomic, copy) NSString *image;
@property (nonatomic, assign, getter=isChecked) BOOL checked; // 自定义批量删除标记

@end

2. 自定义 Cell

// XMGWineCell.h
@interface XMGWineCell : UITableViewCell

@property (nonatomic, strong) XMGWine *wine;

+ (instancetype)cellWithTableView:(UITableView *)tableView;

@end
// XMGWineCell.m
@implementation XMGWineCell

+ (instancetype)cellWithTableView:(UITableView *)tableView {
    static NSString *ID = @"wineCell";
    XMGWineCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (!cell) {
        // 使用 Subtitle 样式,包含 textLabel 和 detailTextLabel
        cell = [[XMGWineCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }
    return cell;
}

- (void)setWine:(XMGWine *)wine {
    _wine = wine;
    self.textLabel.text = wine.name;
    self.detailTextLabel.text = wine.money;
    self.imageView.image = [UIImage imageNamed:wine.image];
    self.accessoryType = wine.isChecked ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
}

@end

3. 控制器与懒加载数据

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) NSMutableArray<XMGWine *> *wineArray;
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (NSMutableArray<XMGWine *> *)wineArray {
    if (!_wineArray) {
        _wineArray = [NSMutableArray array];
        // 从 plist 或网络加载初始数据
    }
    return _wineArray;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
}

@end

二、全局刷新(reloadData)

reloadData 会重新加载整个 TableView,所有 Cell 重新创建和配置。

// 添加
- (IBAction)add {
    XMGWine *wine = [[XMGWine alloc] init];
    wine.money = @"20.5";
    wine.name = @"很好喝的酒";
    wine.image = @"new_wine";
    [self.wineArray insertObject:wine atIndex:0];
    [self.tableView reloadData];
}

// 删除
- (IBAction)remove {
    if (self.wineArray.count > 0) {
        [self.wineArray removeObjectAtIndex:0];
    }
    [self.tableView reloadData];
}

// 修改
- (IBAction)update {
    if (self.wineArray.count > 0) {
        XMGWine *wine = self.wineArray[0];
        wine.money = @"100";
    }
    [self.tableView reloadData];
}

特点:实现简单,但会重新加载所有可见 Cell,无动画过渡,大数据量下性能差。适用于数据全量替换或小数据集。


三、局部刷新

局部刷新只更新变化的行,带有动画过渡,性能远优于全局刷新。

核心原则

  1. 先更新数据源,再更新视图。
  2. 用 beginUpdates / endUpdates 包裹(iOS 11+ 推荐 performBatchUpdates:completion:)。
  3. 插入/删除的 indexPaths 是更新前的位置。

1. 插入行

- (IBAction)add {
    XMGWine *wine1 = [[XMGWine alloc] init];
    wine1.name = @"很好喝的酒"; wine1.money = @"20.5"; wine1.image = @"new_wine";

    XMGWine *wine2 = [[XMGWine alloc] init];
    wine2.name = @"很好"; wine2.money = @"100.5"; wine2.image = @"new_wine";

    NSArray *indexPaths = @[
        [NSIndexPath indexPathForRow:0 inSection:0],
        [NSIndexPath indexPathForRow:1 inSection:0]
    ];

    [self.tableView performBatchUpdates:^{
        [self.wineArray insertObjects:@[wine1, wine2] atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
        [self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationRight];
    } completion:nil];
}

2. 删除行

- (IBAction)remove {
    if (self.wineArray.count < 2) return;

    NSArray *indexPaths = @[
        [NSIndexPath indexPathForRow:0 inSection:0],
        [NSIndexPath indexPathForRow:1 inSection:0]
    ];

    [self.tableView performBatchUpdates:^{
        [self.wineArray removeObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
        [self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationMiddle];
    } completion:nil];
}

3. 刷新行(内容更新)

- (IBAction)update {
    if (self.wineArray.count < 4) return;

    self.wineArray[0].money = @"100";
    self.wineArray[2].image = @"new_wine";
    self.wineArray[3].image = @"new_wine";

    NSArray *indexPaths = @[
        [NSIndexPath indexPathForRow:0 inSection:0],
        [NSIndexPath indexPathForRow:2 inSection:0],
        [NSIndexPath indexPathForRow:3 inSection:0]
    ];

    [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
}

4. 移动行

// 允许移动
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

// 移动完成后更新数据源
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    XMGWine *wine = self.wineArray[sourceIndexPath.row];
    [self.wineArray removeObjectAtIndex:sourceIndexPath.row];
    [self.wineArray insertObject:wine atIndex:destinationIndexPath.row];
}

beginUpdates vs performBatchUpdates

方式 引入版本 特点
beginUpdates / endUpdates iOS 2 同步执行,无 completion 回调
performBatchUpdates:completion: iOS 11 推荐使用,有 completion 回调,动画更流畅

四、左滑操作

1. 系统左滑删除(旧 API)

实现 commitEditingStyle:forRowAtIndexPath: 启用左滑删除:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [self.wineArray removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    }
}

// 自定义删除按钮文字
- (NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath {
    return @"删除";
}

// 控制哪些行可编辑(可选)
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

2. 左滑更多按钮(iOS 8~10:editActionsForRowAtIndexPath)

- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:@"删除" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
        [self.wineArray removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    }];

    UITableViewRowAction *followAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"关注" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
        NSLog(@"点击了关注");
        [tableView setEditing:NO animated:YES];
    }];
    followAction.backgroundColor = [UIColor blueColor];

    // 数组顺序:第一个是最靠近边缘的按钮(最右侧),最后一个是最左侧
    return @[deleteAction, followAction];
}

3. 现代左滑 API(iOS 11+:推荐)

iOS 11 引入 UISwipeActionsConfiguration 和 UIContextualAction,替代已废弃的 editActionsForRowAtIndexPath::

- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
    UIContextualAction *deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"删除" handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) {
        [self.wineArray removeObjectAtIndex:indexPath.row];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
        completionHandler(YES);
    }];

    UIContextualAction *followAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"关注" handler:^(UIContextualAction *action, UIView *sourceView, void (^completionHandler)(BOOL)) {
        NSLog(@"关注");
        completionHandler(YES);
    }];
    followAction.backgroundColor = [UIColor blueColor];

    UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteAction, followAction]];
    config.performsFirstActionWithFullSwipe = YES; // 满滑直接执行第一个动作(删除)
    return config;
}

左侧滑按钮使用 leadingSwipeActionsConfigurationForRowAtIndexPath:。


五、编辑模式与批量删除

1. 进入编辑模式

- (IBAction)toggleEditing {
    [self.tableView setEditing:!self.tableView.isEditing animated:YES];
}

编辑模式下,每行左侧出现删除控制(红色圆点),点击后出现删除按钮。

2. 系统批量删除(多选)

- (void)viewDidLoad {
    [super viewDidLoad];
    // 编辑模式下允许多选
    self.tableView.allowsMultipleSelectionDuringEditing = YES;
}

- (IBAction)batchDelete {
    NSArray<NSIndexPath *> *selectedIndexPaths = self.tableView.indexPathsForSelectedRows;
    if (selectedIndexPaths.count == 0) return;

    // 按 row 降序排列,避免删除时索引错乱
    NSArray *sortedPaths = [selectedIndexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *obj1, NSIndexPath *obj2) {
        return obj2.row < obj1.row; // 降序
    }];

    [self.tableView performBatchUpdates:^{
        for (NSIndexPath *indexPath in sortedPaths) {
            [self.wineArray removeObjectAtIndex:indexPath.row];
        }
        [self.tableView deleteRowsAtIndexPaths:selectedIndexPaths withRowAnimation:UITableViewRowAnimationLeft];
    } completion:^(BOOL finished) {
        [self.tableView setEditing:NO animated:YES];
    }];
}

关键:批量删除数据源时,必须按索引降序删除。如果升序删除,删除第 0 条后,原来的第 1 条变成第 0 条,再删除第 1 条就会跳过或崩溃。

3. 自定义批量删除(非编辑模式)

不依赖系统编辑模式,通过模型的 checked 属性手动管理选中状态:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    XMGWine *wine = self.wineArray[indexPath.row];
    wine.checked = !wine.isChecked;
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}

- (IBAction)customBatchDelete {
    NSMutableArray<XMGWine *> *toDelete = [NSMutableArray array];
    for (XMGWine *wine in self.wineArray) {
        if (wine.isChecked) {
            [toDelete addObject:wine];
        }
    }

    if (toDelete.count == 0) return;

    [self.wineArray removeObjectsInArray:toDelete];

    // 小数据量用 reloadData,大数据量建议用 deleteRows 局部刷新
    [self.tableView reloadData];
}

六、Swift 版本示例

左滑操作(现代 API)

func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let deleteAction = UIContextualAction(style: .destructive, title: "删除") { [weak self] (_, _, completion) in
        guard let self = self else { return }
        self.wineArray.remove(at: indexPath.row)
        tableView.deleteRows(at: [indexPath], with: .left)
        completion(true)
    }

    let followAction = UIContextualAction(style: .normal, title: "关注") { (_, _, completion) in
        print("关注")
        completion(true)
    }
    followAction.backgroundColor = .blue

    let config = UISwipeActionsConfiguration(actions: [deleteAction, followAction])
    config.performsFirstActionWithFullSwipe = true
    return config
}

批量删除

@IBAction func batchDelete() {
    guard let indexPaths = tableView.indexPathsForSelectedRows, !indexPaths.isEmpty else { return }

    let sortedPaths = indexPaths.sorted { $0.row > $1.row } // 降序

    tableView.performBatchUpdates({
        for indexPath in sortedPaths {
            wineArray.remove(at: indexPath.row)
        }
        tableView.deleteRows(at: indexPaths, with: .left)
    }) { _ in
        self.tableView.setEditing(false, animated: true)
    }
}

七、底层逻辑与性能优化

局部刷新的底层机制

  • beginUpdates / endUpdates 将多个插入、删除、移动操作合并为一个动画批次。
  • TableView 在 endUpdates 时统一计算 indexPath 的映射关系,执行动画。
  • 插入/删除的 indexPaths 是更新前的位置,系统内部会自动处理索引偏移。

性能优化建议

场景 推荐方案
全量数据替换 reloadData
少量行增删改 局部刷新(insert/delete/reloadRows)
批量操作 performBatchUpdates 包裹
左滑删除 iOS 11+ UISwipeActionsConfiguration
批量删除 系统多选 + 降序删除数据源
大数据量 避免 reloadData,优先局部刷新

常见坑

  1. 数据源与视图不同步:必须先更新数据源,再调用增删行方法,否则会崩溃(NSInternalInconsistencyException)。
  2. 批量删除索引错乱:必须按降序删除数据源,或先收集要删除的对象再统一移除。
  3. 左滑与编辑模式冲突:左滑删除(trailingSwipeActionsConfiguration)和编辑模式删除可以共存,但需注意交互逻辑。
  4. reloadRows 与 reloadData:reloadRows 只刷新指定行,保留其他行的状态(如滚动位置);reloadData 重置一切。

八、总结

  • 全局刷新(reloadData)简单但低效,适合全量替换。
  • 局部刷新(insertRows / deleteRows / reloadRows)性能好、有动画,是推荐方案,用 performBatchUpdates 包裹。
  • 左滑操作:iOS 11+ 推荐 UISwipeActionsConfiguration + UIContextualAction,旧 API editActionsForRowAtIndexPath: 已废弃。
  • 批量删除:系统多选模式(allowsMultipleSelectionDuringEditing)或自定义 checked 模型;删除数据源时按降序操作避免索引错乱。
  • 核心原则:先更新数据源,再更新视图;保证数据源与视图状态一致。

posted @ 2015-07-24 23:00  Mr.陳  阅读(3210)  评论(0)    收藏  举报