iOS开发之UI篇(10)—— UITableView

版本
Xcode 9.1

一、基本概念

UITableView : UIScrollView : UIView : UIResponder : NSObject

从继承关系我们可以知道:UITableView可以滚动,具有view的性质,能响应UI事件等等~

1. UITableView结构:

UITableView可以有很多个section(本文称为组),每个section分别由一个Header(组头)、一个Fooder(组尾)及若干个cell组成。

UITableView结构图

2. UITableView有两种风格:

typedef NS_ENUM(NSInteger, UITableViewStyle) {
    UITableViewStylePlain,          // regular table view
    UITableViewStyleGrouped         // preferences style table view
};

主要区别为:

  • 当没有设置Header(或Fooder)时,Plain风格会隐藏掉Header(或Fooder),而Grouped仍会显示一个默认高度值。
  • Header(或Fooder)中的字体样式稍有不同。
  • 以前版本来说,使用Plain布局会排到顶部的状态栏,但是新版就没有这个问题了。

3. UITableViewCell有四种风格

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
    UITableViewCellStyleDefault,    // Simple cell with text label and optional image view (behavior of UITableViewCell in iPhoneOS 2.x)
    UITableViewCellStyleValue1,     // Left aligned label on left and right aligned label on right with blue text (Used in Settings)
    UITableViewCellStyleValue2,     // Right aligned label on left with blue text and left aligned label on right (Used in Phone/Contacts)
    UITableViewCellStyleSubtitle    // Left aligned label on top and left aligned label on bottom with gray text (Used in iPod).
};

这四种风格分别对应storyboard/XIB里面的设置:

使用storyboard/XIB设置四种风格

每种风格对应效果图如下:

UITableViewCellStyleDefault
UITableViewCellStyleValue1
UITableViewCellStyleValue2
UITableViewCellStyleSubtitle

如果你的UITableViewCell不显示image或者detailTextLabel,那么最好先检查一下注册的时候设置的style是否符合。

4. 重用cell

为什么要重用cell?
由于屏幕显示的cell有限,而当数据量大的时候,如果每个数据创建一个Cell,就会占很大内存。
重用cell机制是这样的:创建了屏幕内cell所需的个数+1个屏幕之外的cell,在满足显示效果的前提下尽可能降低消耗内存。
这种机制下默认有一个可变数组NSMutableArray* visiableCells用来保存当前显示的cell;一个可变字典NSMutableDictnery* reusableTableCells用来保存可重复利用的cell(之所以用字典是因为可重用的cell有不止一种样式,我们需要根据它的reuseIdentifier,也就是所谓的重用标示符来查找是否有可重用的该样式的cell)。

如何重用cell?
首先要创建一个有重用标识符reuseIdentifier的cell,这些cell会被保存到重用队列中去。当需要实例化cell的时候,首先根据reuseIdentifier去找对应的cell。
加载cell到重用队列有两种方法:

// 法1:使用tableView注册一个cell
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"myCell"];

// 法2:在cell的配置代理方法中创建一个cell
if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
}

根据重用标识符reuseIdentifier实例化一个cell:

// 实例化一个UITableViewCell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath];

二、简单用法

效果图:

代码比较简单,基本上都有注释,直接上完整代码:

#define     TABLE_WIDTH      [UIScreen mainScreen].bounds.size.width
#define     TABLE_HEIGHT     [UIScreen mainScreen].bounds.size.height

#import "ViewController.h"

@interface ViewController () <UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong, nullable) NSArray *digitalArr;
@property (nonatomic, strong, nullable) NSArray *letterArr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 装载数据源
    self.digitalArr = @[@"0", @"1", @"2"];
    self.letterArr  = @[@"A", @"B", @"C"];
    
    // 添加tableView到self.view
    [self.view addSubview:self.tableView];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


#pragma mark - UITableViewDelegate

// cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    return 60;
}


// Header高度
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {

    return 30;
}


// Footer高度
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {

    return 30;
}


// 选中了某个cell
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
}


#pragma mark - UITableViewDataSource

// Section数量
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    
    return 2;
}


// 对应Section中cell的个数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    switch (section) {
        case 0:     // 数字组
            return self.digitalArr.count;
            break;
            
        case 1:     // 字母组
            return self.letterArr.count;
            break;
            
        default:
            break;
    }
    
    return 0;
}


// Header标题
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

    switch (section) {

        case 0:     // 数字组
            return @"数字组头";
            break;

        case 1:     // 字母组
            return @"字母组头";
            break;

        default:
            break;
    }

    return nil;
}


// Footer标题
- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {

    switch (section) {

        case 0:     // 数字组
            return @"数字组尾";
            break;

        case 1:     // 字母组
            return @"字母组尾";
            break;

        default:
            break;
    }

    return nil;
}


// cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 实例化一个UITableViewCell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath];
    // 由于已经注册过重用cell,所以这里不必判断是否存在
//    if (cell == nil) {
//        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
//    }
    
    // 设置cell相关属性
    switch (indexPath.section) {
            
        case 0:     // 数字组
            if (indexPath.row < self.digitalArr.count) {
                cell.textLabel.text = self.digitalArr[indexPath.row];
            }
            break;
            
        case 1:     // 字母组
            if (indexPath.row < self.letterArr.count) {
                cell.textLabel.text = self.letterArr[indexPath.row];
            }
            break;
            
        default:
            break;
    }
    
    // 返回cell
    return cell;
}


#pragma mark - 懒加载

- (UITableView *)tableView {
    
    if (_tableView == nil) {
        // 实例化一个UITableView
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, TABLE_WIDTH, TABLE_HEIGHT)
                                                  style:UITableViewStylePlain];
        // 注册一个cell
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"myCell"];
        // 设置代理
        _tableView.delegate = self;
        _tableView.dataSource = self;
    }
    
    return _tableView;
}


@end

三、cell的增、减、排序

当然,这里要说的操作并不是先改变数据源然后reloadData,而是通过对cell的操作,使整个过程具有动画效果、显得顺畅。

1. 添加

添加

我们会新增两个代理,第一个是返回编辑操作的类型。有插入类型和删除类型。
先定义一个全局变量_isDelete用于标记是点击添加还是删除按钮:

- (IBAction)btnAdd:(UIButton *)sender {
    
    _isDelete = NO;     // 编辑类型:添加
    // 启动编辑(参数一:tableView是否正在编辑;参数二:是否动画)
    [self.tableView setEditing:!self.tableView.isEditing animated:YES];
}

调用方法setEditing: animated:后,接着来到代理方法:

// 返回操作类型(添加/删除)
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"返回操作类型");
    if (_isDelete) {
        return UITableViewCellEditingStyleDelete;
    }
    return UITableViewCellEditingStyleInsert;
}

当点击左边绿色“+”号按钮后,调用此代理方法:

// 提交编辑操作
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"提交编辑操作");
    if (editingStyle == UITableViewCellEditingStyleInsert) {
        switch (indexPath.section) {
                
            case 0:
                [self.digitalArr insertObject:@"我是新插入的数字" atIndex:indexPath.row];
                break;
                
            case 1:
                [self.letterArr insertObject:@"我是新插入的字母" atIndex:indexPath.row];
                break;
                
            default:
                break;
        }
        // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
        [tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    }
}

2. 删除

与添加类似,假如我们设置_isDelete = YES,那么代理返回的是UITableViewCellEditingStyleDelete类型,这时删除是这样子的:

删除

你也不喜欢这种做法对不对?那我们换个做法,直接左滑删除某个cell或者点编辑多选/全选然后批量删除cell。

滑动删除
其实,只要你返回UITableViewCellEditingStyleDelete类型,并实现代理方法tableView : commitEditingStyle: forRowAtIndexPath:,然后就可以向左滑动调出删除按钮了。但是这样一来点击添加按钮就不能调出绿色“+”号按钮了,要解决这个问题,需要修改之前的返回类型的代理方法:

// 返回操作类型(添加/删除)
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    if (!tableView.isEditing) {
        return UITableViewCellEditingStyleDelete;
    }
    
    return UITableViewCellEditingStyleInsert;
}
// 提交编辑操作
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"提交编辑操作");
    if (editingStyle == UITableViewCellEditingStyleInsert) {
        switch (indexPath.section) {
                
            case 0:
                [self.digitalArr insertObject:@"我是新插入的数字" atIndex:indexPath.row];
                break;
                
            case 1:
                [self.letterArr insertObject:@"我是新插入的字母" atIndex:indexPath.row];
                break;
                
            default:
                break;
        }
        // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
        [tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
        
    }else if (editingStyle == UITableViewCellEditingStyleDelete) {
        
        switch (indexPath.section) {
                
            case 0:
                if (indexPath.row < self.digitalArr.count) {
                    [self.digitalArr removeObjectAtIndex:indexPath.row];
                }
                break;
                
            case 1:
                if (indexPath.row < self.letterArr.count) {
                    [self.letterArr removeObjectAtIndex:indexPath.row];
                }
                break;
                
            default:
                break;
        }
        // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    }
}
左滑单个删除

批量删除
改的地方比较多,直接上完整代码了。要注意的是,当section中没有cell时该section也应该删掉,本代码就不演示了。

#define     TABLE_WIDTH      [UIScreen mainScreen].bounds.size.width
#define     TABLE_HEIGHT     [UIScreen mainScreen].bounds.size.height

#import "ViewController.h"

@interface ViewController () <UITableViewDelegate, UITableViewDataSource> {
    
    BOOL    _isDelete;      // 用于区别是添加还是删除操作
    NSMutableArray *_deleteArr;     // 用于储存将要删除的cell的indexPath
}

@property (weak, nonatomic) IBOutlet UIButton *editBtn;
@property (weak, nonatomic) IBOutlet UIButton *addBtn;
@property (weak, nonatomic) IBOutlet UIView *bottomView;
@property (weak, nonatomic) IBOutlet UIButton *allBtn;
@property (weak, nonatomic) IBOutlet UIButton *deleteBtn;

@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong, nullable) NSMutableArray *digitalArr;
@property (nonatomic, strong, nullable) NSMutableArray *letterArr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _isDelete = YES;     // 编辑类型:删除
    self.bottomView.hidden = YES;       //隐藏底部view
    self.deleteBtn.hidden = YES;        //隐藏删除按钮
    
    // 装载数据源
    self.digitalArr = [@[@"0", @"1", @"2"] mutableCopy];
    self.letterArr  = [@[@"A", @"B", @"C"] mutableCopy];
    
    // 添加tableView到self.view
    [self.view addSubview:self.tableView];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


#pragma mark - UITableViewDelegate

// cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    return 60;
}


// Header高度
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {

    return 30;
}


// Footer高度
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {

    return 30;
}


// 选中了某个cell
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 判断是否是多选操作
    if (self.tableView.editing) {
        if (_deleteArr == nil) {
            _deleteArr = [NSMutableArray new];
        }
        // 将选中cell的indexPath添加到删除数组deleteArr
        [_deleteArr addObject:indexPath];
        // 显示删除按钮
        self.deleteBtn.hidden = NO;
        return;
    }
}


// 取消选中时 将存放在deleteArr中的数据移除
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 判断是否是多选操作
    if (self.tableView.editing) {
        //将取消选中的cell的indexPath从数组deleteArr中移除
        [_deleteArr removeObject:indexPath];
        //隐藏删除按钮
        if ([_deleteArr count] == 0) {
            _deleteBtn.hidden = YES;
        }
    }
}


// 返回操作类型(添加/删除)
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"返回操作类型");
    if (!tableView.isEditing) {
        return UITableViewCellEditingStyleDelete;
    }
    
    return UITableViewCellEditingStyleInsert;
}


// 提交编辑操作
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"提交编辑操作");
    if (editingStyle == UITableViewCellEditingStyleInsert) {
        switch (indexPath.section) {
                
            case 0:
                [self.digitalArr insertObject:@"我是新插入的数字" atIndex:indexPath.row];
                break;
                
            case 1:
                [self.letterArr insertObject:@"我是新插入的字母" atIndex:indexPath.row];
                break;
                
            default:
                break;
        }
        // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
        [tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
        
    }else if (editingStyle == UITableViewCellEditingStyleDelete) {
        
        switch (indexPath.section) {
                
            case 0:
                if (indexPath.row < self.digitalArr.count) {
                    [self.digitalArr removeObjectAtIndex:indexPath.row];
                }
                break;
                
            case 1:
                if (indexPath.row < self.letterArr.count) {
                    [self.letterArr removeObjectAtIndex:indexPath.row];
                }
                break;
                
            default:
                break;
        }
        // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    }
}


#pragma mark - UITableViewDataSource

// Section数量
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    
    return 2;
}


// 对应Section中cell的个数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    switch (section) {
        case 0:     // 数字组
            return self.digitalArr.count;
            break;
            
        case 1:     // 字母组
            return self.letterArr.count;
            break;
            
        default:
            break;
    }
    
    return 0;
}


// Header标题
- (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

    switch (section) {

        case 0:     // 数字组
            return @"数字组头";
            break;

        case 1:     // 字母组
            return @"字母组头";
            break;

        default:
            break;
    }

    return nil;
}


// Footer标题
- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {

    switch (section) {

        case 0:     // 数字组
            return @"数字组尾";
            break;

        case 1:     // 字母组
            return @"字母组尾";
            break;

        default:
            break;
    }

    return nil;
}


// cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 实例化一个UITableViewCell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath];
    // 由于已经注册过重用cell,所以这里不必判断是否存在
//    if (cell == nil) {
//        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"];
//    }
    
    // 设置cell相关属性
    switch (indexPath.section) {
            
        case 0:     // 数字组
            if (indexPath.row < self.digitalArr.count) {
                cell.textLabel.text = self.digitalArr[indexPath.row];
            }
            break;
            
        case 1:     // 字母组
            if (indexPath.row < self.letterArr.count) {
                cell.textLabel.text = self.letterArr[indexPath.row];
            }
            break;
            
        default:
            break;
    }
    
    // 返回cell
    return cell;
}


#pragma mark - UI事件

- (IBAction)btnEdit:(UIButton *)sender {
    
    // 如果当前有别人的编辑操作,取消别人的编辑操作,并退出
    if (self.tableView.isEditing && !sender.selected) {
        // 启动编辑(参数一:tableView是否正在编辑;参数二:是否动画)
        [self.tableView setEditing:!self.tableView.isEditing animated:YES];
        return;
    }
    
    // 编辑时显示底部view、不显示添加按钮
    sender.selected = !sender.selected;
    if (sender.selected) {
        self.addBtn.hidden = YES;
        self.deleteBtn.hidden = YES;
        self.deleteBtn.selected = NO;
        self.allBtn.selected = NO;
        self.bottomView.hidden = NO;
    }else {
        // 将要隐藏底部view时,还原各种btn
        self.addBtn.hidden = NO;
        self.deleteBtn.hidden = YES;
        self.deleteBtn.selected = NO;
        self.allBtn.selected = NO;
        self.bottomView.hidden = YES;
    }

    // 支持同时选中多行
    self.tableView.allowsMultipleSelectionDuringEditing = YES;
    // 启动编辑(参数一:tableView是否正在编辑;参数二:是否动画)
    [self.tableView setEditing:!self.tableView.isEditing animated:YES];
    
    
    
}


- (IBAction)btnAdd:(UIButton *)sender {
    
    // 如果当前有别人的编辑操作,取消别人的编辑操作,并退出
    if (self.tableView.isEditing && !sender.selected) {
        // 启动编辑(参数一:tableView是否正在编辑;参数二:是否动画)
        [self.tableView setEditing:!self.tableView.isEditing animated:YES];
        return;
    }
    
    // 编辑时显示底部view、不显示添加按钮
    sender.selected = !sender.selected;
    if (sender.selected) {
        self.editBtn.hidden = YES;
    }else {
        self.editBtn.hidden = NO;
    }
    
    _isDelete = NO;     // 编辑类型:添加
    // 取消支持同时选中多行
    self.tableView.allowsMultipleSelectionDuringEditing = NO;
    // 启动编辑(参数一:tableView是否正在编辑;参数二:是否动画)
    [self.tableView setEditing:!self.tableView.isEditing animated:YES];
}

- (IBAction)btnAll:(UIButton *)sender {
    
    sender.selected = !sender.selected;
    
    if (sender.selected) {
        // 遍历选中数字cell
        for (int i=0; i<self.digitalArr.count; i++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
            // 选中TableView中的cell
            [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
            // 将选中的cell的indexPath存入数组deleteArr
            if (_deleteArr == nil) {
                _deleteArr = [NSMutableArray new];
            }
            [_deleteArr addObject:indexPath];
        }
        // 遍历选中字母cell
        for (int i=0; i<self.letterArr.count; i++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:1];
            // 选中TableView中的cell
            [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
            // 将选中的cell的indexPath存入数组deleteArr
            if (_deleteArr == nil) {
                _deleteArr = [NSMutableArray new];
            }
            [_deleteArr addObject:indexPath];
        }
        // 显示删除按钮
        self.deleteBtn.hidden = NO;
        
    }else {
        // 遍历取消选中数字cell
        for (int i=0; i<self.digitalArr.count; i++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
            //取消选中TableView中的cell
            [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
        }
        // 遍历取消选中字母cell
        for (int i=0; i<self.letterArr.count; i++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:1];
            //取消选中TableView中的cell
            [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
        }
        // 移除数组deleteArr中所有元素(indexPath)
        if (_deleteArr) {
            [_deleteArr removeAllObjects];
        }
        //隐藏删除按钮
        self.deleteBtn.hidden = YES;
    }
}


- (IBAction)btnDelete:(UIButton *)sender {
    
    if (self.tableView.isEditing && _deleteArr) {
        // 此处运用i--是为了防止removeObjectAtIndex出错
        for (NSInteger i=_deleteArr.count-1; i>=0; i--) {
            NSIndexPath *indexPath = _deleteArr[i];
            // 数据源同步删除
            switch (indexPath.section) {
                    
                case 0:     // 数字
                    if (indexPath.row < self.digitalArr.count) {
                        [self.digitalArr removeObjectAtIndex:indexPath.row];
                    }
                    break;
                    
                case 1:     // 字母
                    if (indexPath.row < self.letterArr.count) {
                        [self.letterArr removeObjectAtIndex:indexPath.row];
                    }
                    break;
                    
                default:
                    break;
            }
            // 与reloadData不同,此方法带动画效果.(参数二为动画类型)
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
        }
        [_deleteArr removeAllObjects];
    }
}


#pragma mark - 懒加载

- (UITableView *)tableView {
    
    if (_tableView == nil) {
        // 实例化一个UITableView
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, TABLE_WIDTH, TABLE_HEIGHT-64-46)
                                                  style:UITableViewStylePlain];
        // 注册一个cell
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"myCell"];
        // 设置代理
        _tableView.delegate = self;
        _tableView.dataSource = self;
    }
    
    return _tableView;
}


@end
批量删除

3. 排序

和增加、删除相似,要实现排序功能只需实现下面这个协议即可:

/**
 移动cell

 @param tableView 正在操作的tableView对象
 @param sourceIndexPath 移动前的indexPath
 @param destinationIndexPath 移动后的indexPath
 */
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
    
    // 需要改变数据源的排序(略)
}
排序

四、几种方法创建cell

常用cell的创建方法有两种,一种是使用系统cell,另一种是自定义cell。其中自定义cell又可以分为三种:纯代码创建cell,XIB创建cell,storyboard创建cell。

  1. 使用系统cell
  2. 自定义cell之纯代码
  3. 自定义cell之XIB
  4. 自定义cell之storyboard

1. 使用系统cell

其实我们上面例子中使用的就是系统cell:通过注册一个UITableViewCell,然后在tableView: cellForRowAtIndexPath:方法里返回一个已经注册到重用队列里的UITableViewCell。

// 注册一个cell
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"myCell"];

// 在tableView: cellForRowAtIndexPath:方法里实例化一个UITableViewCell,并返回
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath];
return cell;

2. 自定义cell之纯代码

首先,我们先新建一个继承自UITableViewCell的类MyTableViewCell:

新建一个MyTableViewCell

然后倒入头文件#import "MyTableViewCell.h",接下来将上面的系统cell改为我们新建的自定义cell就好了:

// 注册一个cell
[_tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"myCell"];

// 在tableView: cellForRowAtIndexPath:方法里实例化一个MyTableViewCell,并返回
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath];
return cell;

我们可以重写一下cell,来验证是否是使用自定义cell。因为cell继承自UIView,因此我们可以加入如下方法:

- (void)drawRect:(CGRect)rect {
    
    self.backgroundColor = [UIColor redColor];
}

运行结果:

纯代码创建cell

3. 自定义cell之XIB

和纯代码创建类似,只需要在创建的时候勾选创建XIB文件:

使用XIB创建cell

然后我们就会看到XIB里面cell已经自动关联到类MyXibTableViewCell了:

XIB的类名

当然,如果我们创建的时候不勾选Also create XIB file选项,也是可以后续单独创建XIB的,只是创建好的XIB要手动关联到我们自定义cell的类名。
接下来,我们为XIB的cell起一个标识符:

XIB中cell的标识符

最后,我们在注册cell的时候,使用这个标识符找到XIB中的cell,从而创建使用。

// 注册一个cell
[_tableView registerNib:[UINib nibWithNibName:@"MyXibTableViewCell" bundle:nil] forCellReuseIdentifier:@"MyXibTableViewCell"];

在tableView: cellForRowAtIndexPath:方法里实例化一个MyXibTableViewCell,并返回:

// 返回MyXibTableViewCell
- (MyXibTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 实例化一个MyXibTableViewCell
    MyXibTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyXibTableViewCell"];
    
    // 设置cell相关属性
    
    // 返回cell
    return cell;
}

为了验证是否使用XIB的cell,设置cell的背景颜色:

设置XIB中cell的背景颜色

运行结果:

XIB创建cell

4. 自定义cell之storyboard

先在Main.storyboard里面创建一个tableview和一个cell:

Main.storyboard

设置tableview的dalegate和datasource:

设置dalegate和datasource

设置类别:

class

设置id:

id

最后,我们可以省掉注册cell这一步(因为系统会自动帮我们注册,噢耶~),直接在tableView: cellForRowAtIndexPath:方法里实返回MyStoryboardTableViewCell

// cell
- (MyStoryboardTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    // 实例化一个MyStoryboardTableViewCell
    MyStoryboardTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyStoryboardTableViewCell"];
    
    // 设置cell相关属性
    if (indexPath.row < self.digitalArr.count) {
        cell.textLabel.text = self.digitalArr[indexPath.row];
    }
    
    // 返回cell
    return cell;
}

设置storyboard里cell的背景颜色:

设置背景颜色

运行结果:

storyboard创建cell

五、自定义Header/Footer

如同自定义cell,上面的代码中返回cell的时候返回我们自己创建的cell就好了。对于Header/Footer也一样,我们首先要分别实现两个代理方法:一个返回高度,一个返回内容(UIView类型)。

// Header高度
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {

    return 100;
}


// Footer高度
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {

    return 30;
}


// 自定义Header
- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TABLE_WIDTH, 100)];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:view.bounds];
    imageView.image = [UIImage imageNamed:@"mas091"];
    [view addSubview:imageView];
    
    return view;
}


// 自定义Footer
- (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, TABLE_WIDTH, 30)];
    view.backgroundColor = [UIColor blackColor];
    
    return view;
}

效果图:

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

推荐阅读更多精彩内容